Skip to content

Commit

Permalink
Merge pull request #1964 from get10101/feat/in-app-polls
Browse files Browse the repository at this point in the history
feat(mobile): show in app survey
  • Loading branch information
bonomat authored Feb 6, 2024
2 parents 8f15686 + c4bf8ae commit 3ffb258
Show file tree
Hide file tree
Showing 24 changed files with 940 additions and 11 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Feat(mobile): Add in-app survey feature. The coordinator can trigger surveys which will be shown in the app.

## [1.8.5] - 2024-02-05

- Feat(webapp): Show order history
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- This file should undo anything in `up.sql`
drop table answers;
drop table choices;
drop table polls;
DROP TYPE IF EXISTS "Poll_Type_Type";
37 changes: 37 additions & 0 deletions coordinator/migrations/2024-02-02-075927_add_polls_table/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
-- Your SQL goes here

CREATE TYPE "Poll_Type_Type" AS ENUM ('SingleChoice');

CREATE TABLE polls
(
id SERIAL PRIMARY KEY NOT NULL,
poll_type "Poll_Type_Type" NOT NULL,
question TEXT NOT NULL,
active BOOLEAN NOT NULL,
creation_timestamp timestamp WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE choices
(
id SERIAL PRIMARY KEY NOT NULL,
poll_id SERIAL REFERENCES polls (id),
value TEXT NOT NULL
);

CREATE TABLE answers
(
id SERIAL PRIMARY KEY NOT NULL,
choice_id SERIAL REFERENCES choices (id),
trader_pubkey TEXT NOT NULL REFERENCES users (pubkey),
value TEXT NOT NULL,
creation_timestamp timestamp WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO polls (poll_type, question, active)
VALUES ('SingleChoice', 'Where did you hear about us?', true);

INSERT INTO choices (poll_id, value)
VALUES (1, 'Social media (X.com, Nostr)'),
(1, 'Search engine (Google, Duckduckgo)'),
(1, 'Friends'),
(1, 'other');
20 changes: 20 additions & 0 deletions coordinator/src/db/custom_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::db::channels::ChannelState;
use crate::db::dlc_messages::MessageType;
use crate::db::payments::HtlcStatus;
use crate::db::payments::PaymentFlow;
use crate::db::polls::PollType;
use crate::db::positions::ContractSymbol;
use crate::db::positions::PositionState;
use crate::schema::sql_types::ChannelStateType;
Expand All @@ -10,6 +11,7 @@ use crate::schema::sql_types::DirectionType;
use crate::schema::sql_types::HtlcStatusType;
use crate::schema::sql_types::MessageTypeType;
use crate::schema::sql_types::PaymentFlowType;
use crate::schema::sql_types::PollTypeType;
use crate::schema::sql_types::PositionStateType;
use diesel::deserialize;
use diesel::deserialize::FromSql;
Expand Down Expand Up @@ -205,3 +207,21 @@ impl FromSql<MessageTypeType, Pg> for MessageType {
}
}
}

impl ToSql<PollTypeType, Pg> for PollType {
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
match *self {
PollType::SingleChoice => out.write_all(b"SingleChoice")?,
}
Ok(IsNull::No)
}
}

impl FromSql<PollTypeType, Pg> for PollType {
fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<Self> {
match bytes.as_bytes() {
b"SingleChoice" => Ok(PollType::SingleChoice),
_ => Err("Unrecognized enum variant for PollType".into()),
}
}
}
1 change: 1 addition & 0 deletions coordinator/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod legacy_collaborative_reverts;
pub mod liquidity;
pub mod liquidity_options;
pub mod payments;
pub mod polls;
pub mod positions;
pub mod positions_helper;
pub mod routing_fees;
Expand Down
135 changes: 135 additions & 0 deletions coordinator/src/db/polls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use crate::schema::answers;
use crate::schema::choices;
use crate::schema::polls;
use crate::schema::sql_types::PollTypeType;
use anyhow::bail;
use anyhow::Result;
use diesel::query_builder::QueryId;
use diesel::AsExpression;
use diesel::FromSqlRow;
use diesel::Identifiable;
use diesel::Insertable;
use diesel::PgConnection;
use diesel::QueryDsl;
use diesel::QueryResult;
use diesel::Queryable;
use diesel::RunQueryDsl;
use diesel::Selectable;
use diesel::SelectableHelper;
use std::any::TypeId;
use std::collections::HashMap;
use time::OffsetDateTime;

#[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression, Eq, Hash)]
#[diesel(sql_type = PollTypeType)]
pub enum PollType {
SingleChoice,
}

impl QueryId for PollTypeType {
type QueryId = PollTypeType;
const HAS_STATIC_QUERY_ID: bool = false;

fn query_id() -> Option<TypeId> {
None
}
}

#[derive(Insertable, Queryable, Identifiable, Selectable, Debug, Clone, Eq, PartialEq, Hash)]
#[diesel(table_name = polls)]
#[diesel(primary_key(id))]
pub struct Poll {
pub id: i32,
pub poll_type: PollType,
pub question: String,
pub active: bool,
pub creation_timestamp: OffsetDateTime,
}

#[derive(Insertable, Queryable, Identifiable, Selectable, Debug, Clone, Eq, PartialEq)]
#[diesel(belongs_to(Poll))]
#[diesel(table_name = choices)]
#[diesel(primary_key(id))]
pub struct Choice {
pub id: i32,
pub poll_id: i32,
pub value: String,
}
#[derive(Insertable, Queryable, Identifiable, Debug, Clone)]
#[diesel(primary_key(id))]
pub struct Answer {
pub id: Option<i32>,
pub choice_id: i32,
pub trader_pubkey: String,
pub value: String,
pub creation_timestamp: OffsetDateTime,
}

pub fn active(conn: &mut PgConnection) -> QueryResult<Vec<commons::Poll>> {
let _polls: Vec<Poll> = polls::table.load(conn)?;
let _choices: Vec<Choice> = choices::table.load(conn)?;

let results = polls::table
.left_join(choices::table)
.select(<(Poll, Option<Choice>)>::as_select())
.load::<(Poll, Option<Choice>)>(conn)?;

let mut polls_with_choices = HashMap::new();
for (poll, choice) in results {
let entry = polls_with_choices.entry(poll).or_insert_with(Vec::new);
if let Some(choice) = choice {
entry.push(choice);
}
}

let polls = polls_with_choices
.into_iter()
.map(|(poll, choice_vec)| commons::Poll {
id: poll.id,
poll_type: poll.poll_type.into(),
question: poll.question,
choices: choice_vec
.into_iter()
.map(|choice| commons::Choice {
id: choice.id,
value: choice.value,
})
.collect(),
})
.collect();
Ok(polls)
}

impl From<PollType> for commons::PollType {
fn from(value: PollType) -> Self {
match value {
PollType::SingleChoice => commons::PollType::SingleChoice,
}
}
}

pub fn add_answer(conn: &mut PgConnection, answers: commons::PollAnswers) -> Result<()> {
let mut affected_rows = 0;
for answer in answers.answers {
affected_rows += diesel::insert_into(answers::table)
.values(Answer {
id: None,
choice_id: answer.choice_id,
trader_pubkey: answers.trader_pk.to_string(),
value: answer.value,
creation_timestamp: OffsetDateTime::now_utc(),
})
.execute(conn)?;
}

if affected_rows == 0 {
bail!(
"Could not insert answers by user {}.",
answers.trader_pk.to_string()
);
} else {
tracing::trace!(%affected_rows, trade_pk = answers.trader_pk.to_string(),
"Added new answers to a poll.");
}
Ok(())
}
34 changes: 34 additions & 0 deletions coordinator/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ use commons::CollaborativeRevertTraderResponse;
use commons::DeleteBackup;
use commons::Message;
use commons::OnboardingParam;
use commons::Poll;
use commons::PollAnswers;
use commons::RegisterParams;
use commons::Restore;
use commons::RouteHintHop;
Expand Down Expand Up @@ -132,6 +134,7 @@ pub fn router(
Router::new()
.route("/", get(index))
.route("/api/version", get(version))
.route("/api/polls", get(get_polls).post(post_poll_answer))
.route(
"/api/fee_rate_estimate/:target",
get(get_fee_rate_estimation),
Expand Down Expand Up @@ -523,6 +526,37 @@ pub async fn version() -> Result<Json<Version>, AppError> {
}))
}

pub async fn get_polls(State(state): State<Arc<AppState>>) -> Result<Json<Vec<Poll>>, AppError> {
let mut connection = state
.pool
.get()
.map_err(|_| AppError::InternalServerError("Could not get db connection".to_string()))?;
let polls = db::polls::active(&mut connection).map_err(|error| {
AppError::InternalServerError(format!("Could not fetch new polls {error}"))
})?;
Ok(Json(polls))
}
pub async fn post_poll_answer(
State(state): State<Arc<AppState>>,
poll_answer: Json<PollAnswers>,
) -> Result<(), AppError> {
tracing::trace!(
poll_id = poll_answer.poll_id,
trader_pk = poll_answer.trader_pk.to_string(),
answers = ?poll_answer.answers,
"Received new answer");
let mut connection = state
.pool
.get()
.map_err(|_| AppError::InternalServerError("Could not get db connection".to_string()))?;

db::polls::add_answer(&mut connection, poll_answer.0).map_err(|error| {
AppError::InternalServerError(format!("Could not save answer in db: {error:?}"))
})?;

Ok(())
}

#[instrument(skip_all, err(Debug))]
pub async fn collaborative_revert_confirm(
State(state): State<Arc<AppState>>,
Expand Down
40 changes: 40 additions & 0 deletions coordinator/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,25 @@ pub mod sql_types {
#[diesel(postgres_type(name = "Payment_Flow_Type"))]
pub struct PaymentFlowType;

#[derive(diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "Poll_Type_Type"))]
pub struct PollTypeType;

#[derive(diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "PositionState_Type"))]
pub struct PositionStateType;
}

diesel::table! {
answers (id) {
id -> Int4,
choice_id -> Int4,
trader_pubkey -> Text,
value -> Text,
creation_timestamp -> Timestamptz,
}
}

diesel::table! {
use diesel::sql_types::*;
use super::sql_types::ChannelStateType;
Expand All @@ -65,6 +79,14 @@ diesel::table! {
}
}

diesel::table! {
choices (id) {
id -> Int4,
poll_id -> Int4,
value -> Text,
}
}

diesel::table! {
collaborative_reverts (id) {
id -> Int4,
Expand Down Expand Up @@ -209,6 +231,19 @@ diesel::table! {
}
}

diesel::table! {
use diesel::sql_types::*;
use super::sql_types::PollTypeType;

polls (id) {
id -> Int4,
poll_type -> PollTypeType,
question -> Text,
active -> Bool,
creation_timestamp -> Timestamptz,
}
}

diesel::table! {
use diesel::sql_types::*;
use super::sql_types::ContractSymbolType;
Expand Down Expand Up @@ -301,12 +336,16 @@ diesel::table! {
}
}

diesel::joinable!(answers -> choices (choice_id));
diesel::joinable!(choices -> polls (poll_id));
diesel::joinable!(last_outbound_dlc_messages -> dlc_messages (message_hash));
diesel::joinable!(liquidity_request_logs -> liquidity_options (liquidity_option));
diesel::joinable!(trades -> positions (position_id));

diesel::allow_tables_to_appear_in_same_query!(
answers,
channels,
choices,
collaborative_reverts,
dlc_messages,
last_outbound_dlc_messages,
Expand All @@ -316,6 +355,7 @@ diesel::allow_tables_to_appear_in_same_query!(
matches,
orders,
payments,
polls,
positions,
routing_fees,
spendable_outputs,
Expand Down
2 changes: 2 additions & 0 deletions crates/commons/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod liquidity_option;
mod message;
mod order;
mod order_matching_fee;
mod polls;
mod price;
mod rollover;
mod route;
Expand All @@ -21,6 +22,7 @@ pub use crate::liquidity_option::*;
pub use crate::message::*;
pub use crate::order::*;
pub use crate::order_matching_fee::order_matching_fee_taker;
pub use crate::polls::*;
pub use crate::price::best_current_price;
pub use crate::price::Price;
pub use crate::price::Prices;
Expand Down
Loading

0 comments on commit 3ffb258

Please sign in to comment.