matrix_sdk_ui/timeline/event_item/content/
polls.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! This module handles rendering of MSC3381 polls in the timeline.
16
17use std::collections::HashMap;
18
19use ruma::{
20    events::poll::{
21        compile_unstable_poll_results,
22        start::PollKind,
23        unstable_response::UnstablePollResponseEventContent,
24        unstable_start::{
25            NewUnstablePollStartEventContent, NewUnstablePollStartEventContentWithoutRelation,
26            UnstablePollStartContentBlock,
27        },
28        PollResponseData,
29    },
30    MilliSecondsSinceUnixEpoch, OwnedUserId, UserId,
31};
32
33/// Holds the state of a poll.
34///
35/// This struct should be created for each poll start event handled and then
36/// updated whenever handling any poll response or poll end event that relates
37/// to the same poll start event.
38#[derive(Clone, Debug)]
39pub struct PollState {
40    pub(in crate::timeline) start_event_content: NewUnstablePollStartEventContent,
41    pub(in crate::timeline) response_data: Vec<ResponseData>,
42    pub(in crate::timeline) end_event_timestamp: Option<MilliSecondsSinceUnixEpoch>,
43    pub(in crate::timeline) has_been_edited: bool,
44}
45
46#[derive(Clone, Debug)]
47pub(in crate::timeline) struct ResponseData {
48    pub sender: OwnedUserId,
49    pub timestamp: MilliSecondsSinceUnixEpoch,
50    pub answers: Vec<String>,
51}
52
53impl PollState {
54    pub(crate) fn new(
55        content: NewUnstablePollStartEventContent,
56        edit: Option<NewUnstablePollStartEventContentWithoutRelation>,
57    ) -> Self {
58        let mut ret = Self {
59            start_event_content: content,
60            response_data: vec![],
61            end_event_timestamp: None,
62            has_been_edited: false,
63        };
64
65        if let Some(edit) = edit {
66            // SAFETY: [`Self::edit`] only returns `None` when the poll has ended, not the
67            // case here.
68            ret = ret.edit(edit).unwrap();
69        }
70
71        ret
72    }
73
74    /// Applies an edit to a poll, returns `None` if the poll was already marked
75    /// as finished.
76    pub(crate) fn edit(
77        &self,
78        replacement: NewUnstablePollStartEventContentWithoutRelation,
79    ) -> Option<Self> {
80        if self.end_event_timestamp.is_none() {
81            let mut clone = self.clone();
82            clone.start_event_content.poll_start = replacement.poll_start;
83            clone.start_event_content.text = replacement.text;
84            clone.has_been_edited = true;
85            Some(clone)
86        } else {
87            None
88        }
89    }
90
91    pub(crate) fn add_response(
92        &self,
93        sender: &UserId,
94        timestamp: MilliSecondsSinceUnixEpoch,
95        content: &UnstablePollResponseEventContent,
96    ) -> Self {
97        let mut clone = self.clone();
98        clone.response_data.push(ResponseData {
99            sender: sender.to_owned(),
100            timestamp,
101            answers: content.poll_response.answers.clone(),
102        });
103        clone
104    }
105
106    /// Marks the poll as ended.
107    ///
108    /// If the poll has already ended, returns `Err(())`.
109    pub(crate) fn end(&self, timestamp: MilliSecondsSinceUnixEpoch) -> Result<Self, ()> {
110        if self.end_event_timestamp.is_none() {
111            let mut clone = self.clone();
112            clone.end_event_timestamp = Some(timestamp);
113            Ok(clone)
114        } else {
115            Err(())
116        }
117    }
118
119    pub fn fallback_text(&self) -> Option<String> {
120        self.start_event_content.text.clone()
121    }
122
123    pub fn results(&self) -> PollResult {
124        let results = compile_unstable_poll_results(
125            &self.start_event_content.poll_start,
126            self.response_data.iter().map(|response_data| PollResponseData {
127                sender: &response_data.sender,
128                origin_server_ts: response_data.timestamp,
129                selections: &response_data.answers,
130            }),
131            self.end_event_timestamp,
132        );
133
134        PollResult {
135            question: self.start_event_content.poll_start.question.text.clone(),
136            kind: self.start_event_content.poll_start.kind.clone(),
137            max_selections: self.start_event_content.poll_start.max_selections.into(),
138            answers: self
139                .start_event_content
140                .poll_start
141                .answers
142                .iter()
143                .map(|i| PollResultAnswer { id: i.id.clone(), text: i.text.clone() })
144                .collect(),
145            votes: results
146                .iter()
147                .map(|i| ((*i.0).to_owned(), i.1.iter().map(|i| i.to_string()).collect()))
148                .collect(),
149            end_time: self.end_event_timestamp,
150            has_been_edited: self.has_been_edited,
151        }
152    }
153
154    /// Returns true whether this poll has been edited.
155    pub fn is_edit(&self) -> bool {
156        self.has_been_edited
157    }
158}
159
160impl From<PollState> for NewUnstablePollStartEventContent {
161    fn from(value: PollState) -> Self {
162        let content = UnstablePollStartContentBlock::new(
163            value.start_event_content.poll_start.question.text.clone(),
164            value.start_event_content.poll_start.answers.clone(),
165        );
166        if let Some(text) = value.fallback_text() {
167            NewUnstablePollStartEventContent::plain_text(text, content)
168        } else {
169            NewUnstablePollStartEventContent::new(content)
170        }
171    }
172}
173
174#[derive(Debug)]
175pub struct PollResult {
176    pub question: String,
177    pub kind: PollKind,
178    pub max_selections: u64,
179    pub answers: Vec<PollResultAnswer>,
180    pub votes: HashMap<String, Vec<String>>,
181    pub end_time: Option<MilliSecondsSinceUnixEpoch>,
182    pub has_been_edited: bool,
183}
184
185#[derive(Debug)]
186pub struct PollResultAnswer {
187    pub id: String,
188    pub text: String,
189}