Skip to main content

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    MilliSecondsSinceUnixEpoch, OwnedUserId, UserId,
21    events::poll::{
22        PollResponseData, compile_unstable_poll_results,
23        start::PollKind,
24        unstable_start::{
25            NewUnstablePollStartEventContent, NewUnstablePollStartEventContentWithoutRelation,
26            UnstablePollStartContentBlock,
27        },
28    },
29};
30
31/// Holds the state of a poll.
32///
33/// This struct should be created for each poll start event handled and then
34/// updated whenever handling any poll response or poll end event that relates
35/// to the same poll start event.
36#[derive(Clone, Debug)]
37pub struct PollState {
38    /// Text representation of the message, for clients that don't support
39    /// polls.
40    pub(in crate::timeline) fallback_text: Option<String>,
41    /// The poll content of the message.
42    pub(in crate::timeline) poll_start: UnstablePollStartContentBlock,
43    pub(in crate::timeline) response_data: Vec<ResponseData>,
44    pub(in crate::timeline) end_event_timestamp: Option<MilliSecondsSinceUnixEpoch>,
45    pub(in crate::timeline) has_been_edited: bool,
46}
47
48#[derive(Clone, Debug)]
49pub(in crate::timeline) struct ResponseData {
50    pub sender: OwnedUserId,
51    pub timestamp: MilliSecondsSinceUnixEpoch,
52    pub answers: Vec<String>,
53}
54
55impl PollState {
56    pub(crate) fn new(
57        poll_start: UnstablePollStartContentBlock,
58        fallback_text: Option<String>,
59    ) -> Self {
60        Self {
61            fallback_text,
62            poll_start,
63            response_data: vec![],
64            end_event_timestamp: None,
65            has_been_edited: false,
66        }
67    }
68
69    /// Applies an edit to a poll, returns `None` if the poll was already marked
70    /// as finished.
71    pub(crate) fn edit(
72        &self,
73        replacement: NewUnstablePollStartEventContentWithoutRelation,
74    ) -> Option<Self> {
75        if self.end_event_timestamp.is_none() {
76            let mut clone = self.clone();
77            clone.poll_start = replacement.poll_start;
78            clone.fallback_text = replacement.text;
79            clone.has_been_edited = true;
80            Some(clone)
81        } else {
82            None
83        }
84    }
85
86    /// Reverts an edit applied with [`Self::edit`], by restoring the fields it
87    /// replaced from `unedited`.
88    ///
89    /// Responses are left alone: they're part of the state an edit carries
90    /// over, so the ones received since the edit was applied are kept.
91    pub(crate) fn unedit(&self, unedited: &Self) -> Self {
92        let mut clone = self.clone();
93        clone.poll_start = unedited.poll_start.clone();
94        clone.fallback_text = unedited.fallback_text.clone();
95        clone.has_been_edited = unedited.has_been_edited;
96        clone
97    }
98
99    /// Add a response to a poll.
100    pub(crate) fn add_response(
101        &mut self,
102        sender: OwnedUserId,
103        timestamp: MilliSecondsSinceUnixEpoch,
104        answers: Vec<String>,
105    ) {
106        self.response_data.push(ResponseData { sender, timestamp, answers });
107    }
108
109    /// Remove a response from the poll, as identified by its sender and
110    /// timestamp values.
111    pub(crate) fn remove_response(
112        &mut self,
113        sender: &UserId,
114        timestamp: MilliSecondsSinceUnixEpoch,
115    ) {
116        if let Some(idx) = self
117            .response_data
118            .iter()
119            .position(|resp| resp.sender == sender && resp.timestamp == timestamp)
120        {
121            self.response_data.remove(idx);
122        }
123    }
124
125    /// Marks the poll as ended.
126    ///
127    /// Returns false if the poll was already ended, true otherwise.
128    pub(crate) fn end(&mut self, timestamp: MilliSecondsSinceUnixEpoch) -> bool {
129        if self.end_event_timestamp.is_none() {
130            self.end_event_timestamp = Some(timestamp);
131            true
132        } else {
133            false
134        }
135    }
136
137    /// Get the text representation of the poll, for clients that don't support
138    /// them.
139    pub fn fallback_text(&self) -> Option<String> {
140        self.fallback_text.clone()
141    }
142
143    /// Compute the current results of the poll.
144    pub fn results(&self) -> PollResult {
145        let results = compile_unstable_poll_results(
146            &self.poll_start,
147            self.response_data.iter().map(|response_data| PollResponseData {
148                sender: &response_data.sender,
149                origin_server_ts: response_data.timestamp,
150                selections: &response_data.answers,
151            }),
152            self.end_event_timestamp,
153        );
154
155        PollResult {
156            question: self.poll_start.question.text.clone(),
157            kind: self.poll_start.kind.clone(),
158            max_selections: self.poll_start.max_selections.into(),
159            answers: self
160                .poll_start
161                .answers
162                .iter()
163                .map(|i| PollResultAnswer { id: i.id.clone(), text: i.text.clone() })
164                .collect(),
165            votes: results
166                .into_iter()
167                .map(|(id, users)| {
168                    (id.to_owned(), users.into_iter().map(ToOwned::to_owned).collect())
169                })
170                .collect(),
171            end_time: self.end_event_timestamp,
172            has_been_edited: self.has_been_edited,
173        }
174    }
175
176    /// Returns true whether this poll has been edited.
177    pub fn is_edit(&self) -> bool {
178        self.has_been_edited
179    }
180}
181
182impl From<PollState> for NewUnstablePollStartEventContent {
183    fn from(value: PollState) -> Self {
184        let content = UnstablePollStartContentBlock::new(
185            value.poll_start.question.text.clone(),
186            value.poll_start.answers.clone(),
187        );
188        if let Some(text) = value.fallback_text() {
189            NewUnstablePollStartEventContent::plain_text(text, content)
190        } else {
191            NewUnstablePollStartEventContent::new(content)
192        }
193    }
194}
195
196/// The results of a poll.
197#[derive(Debug)]
198pub struct PollResult {
199    /// The question of the poll.
200    pub question: String,
201    /// The kind of the poll.
202    pub kind: PollKind,
203    /// The maximum number of responses a user is able to select.
204    pub max_selections: u64,
205    /// The possible answers to the poll.
206    pub answers: Vec<PollResultAnswer>,
207    /// The current votes of the poll.
208    ///
209    /// This is a map from answer ID to the list of user IDs that voted for that
210    /// answer.
211    pub votes: HashMap<String, Vec<OwnedUserId>>,
212    /// The time when the poll ended.
213    ///
214    /// If this is set the poll has ended, otherwise the poll is still ongoing.
215    pub end_time: Option<MilliSecondsSinceUnixEpoch>,
216    /// Whether the poll was edited.
217    pub has_been_edited: bool,
218}
219
220/// A possible answer of a poll.
221#[derive(Debug)]
222pub struct PollResultAnswer {
223    /// The ID of the answer.
224    ///
225    /// This is the string used to identify an answer when voting.
226    pub id: String,
227    /// The text representation of the answer.
228    ///
229    /// This is the string to present to the user in the UI.
230    pub text: String,
231}