Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mobile): show in app survey #1964

Merged
merged 2 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
bonomat marked this conversation as resolved.
Show resolved Hide resolved
);

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
Loading