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_start::{
24            NewUnstablePollStartEventContent, NewUnstablePollStartEventContentWithoutRelation,
25            UnstablePollStartContentBlock,
26        },
27        PollResponseData,
28    },
29    MilliSecondsSinceUnixEpoch, OwnedUserId, UserId,
30};
31
32/// Holds the state of a poll.
33///
34/// This struct should be created for each poll start event handled and then
35/// updated whenever handling any poll response or poll end event that relates
36/// to the same poll start event.
37#[derive(Clone, Debug)]
38pub struct PollState {
39    pub(in crate::timeline) start_event_content: NewUnstablePollStartEventContent,
40    pub(in crate::timeline) response_data: Vec<ResponseData>,
41    pub(in crate::timeline) end_event_timestamp: Option<MilliSecondsSinceUnixEpoch>,
42    pub(in crate::timeline) has_been_edited: bool,
43}
44
45#[derive(Clone, Debug)]
46pub(in crate::timeline) struct ResponseData {
47    pub sender: OwnedUserId,
48    pub timestamp: MilliSecondsSinceUnixEpoch,
49    pub answers: Vec<String>,
50}
51
52impl PollState {
53    pub(crate) fn new(content: NewUnstablePollStartEventContent) -> Self {
54        Self {
55            start_event_content: content,
56            response_data: vec![],
57            end_event_timestamp: None,
58            has_been_edited: false,
59        }
60    }
61
62    /// Applies an edit to a poll, returns `None` if the poll was already marked
63    /// as finished.
64    pub(crate) fn edit(
65        &self,
66        replacement: NewUnstablePollStartEventContentWithoutRelation,
67    ) -> Option<Self> {
68        if self.end_event_timestamp.is_none() {
69            let mut clone = self.clone();
70            clone.start_event_content.poll_start = replacement.poll_start;
71            clone.start_event_content.text = replacement.text;
72            clone.has_been_edited = true;
73            Some(clone)
74        } else {
75            None
76        }
77    }
78
79    /// Add a response to a poll.
80    pub(crate) fn add_response(
81        &mut self,
82        sender: OwnedUserId,
83        timestamp: MilliSecondsSinceUnixEpoch,
84        answers: Vec<String>,
85    ) {
86        self.response_data.push(ResponseData { sender, timestamp, answers });
87    }
88
89    /// Remove a response from the poll, as identified by its sender and
90    /// timestamp values.
91    pub(crate) fn remove_response(
92        &mut self,
93        sender: &UserId,
94        timestamp: MilliSecondsSinceUnixEpoch,
95    ) {
96        if let Some(idx) = self
97            .response_data
98            .iter()
99            .position(|resp| resp.sender == sender && resp.timestamp == timestamp)
100        {
101            self.response_data.remove(idx);
102        }
103    }
104
105    /// Marks the poll as ended.
106    ///
107    /// Returns false if the poll was already ended, true otherwise.
108    pub(crate) fn end(&mut self, timestamp: MilliSecondsSinceUnixEpoch) -> bool {
109        if self.end_event_timestamp.is_none() {
110            self.end_event_timestamp = Some(timestamp);
111            true
112        } else {
113            false
114        }
115    }
116
117    pub fn fallback_text(&self) -> Option<String> {
118        self.start_event_content.text.clone()
119    }
120
121    pub fn results(&self) -> PollResult {
122        let results = compile_unstable_poll_results(
123            &self.start_event_content.poll_start,
124            self.response_data.iter().map(|response_data| PollResponseData {
125                sender: &response_data.sender,
126                origin_server_ts: response_data.timestamp,
127                selections: &response_data.answers,
128            }),
129            self.end_event_timestamp,
130        );
131
132        PollResult {
133            question: self.start_event_content.poll_start.question.text.clone(),
134            kind: self.start_event_content.poll_start.kind.clone(),
135            max_selections: self.start_event_content.poll_start.max_selections.into(),
136            answers: self
137                .start_event_content
138                .poll_start
139                .answers
140                .iter()
141                .map(|i| PollResultAnswer { id: i.id.clone(), text: i.text.clone() })
142                .collect(),
143            votes: results
144                .iter()
145                .map(|i| ((*i.0).to_owned(), i.1.iter().map(|i| i.to_string()).collect()))
146                .collect(),
147            end_time: self.end_event_timestamp,
148            has_been_edited: self.has_been_edited,
149        }
150    }
151
152    /// Returns true whether this poll has been edited.
153    pub fn is_edit(&self) -> bool {
154        self.has_been_edited
155    }
156}
157
158impl From<PollState> for NewUnstablePollStartEventContent {
159    fn from(value: PollState) -> Self {
160        let content = UnstablePollStartContentBlock::new(
161            value.start_event_content.poll_start.question.text.clone(),
162            value.start_event_content.poll_start.answers.clone(),
163        );
164        if let Some(text) = value.fallback_text() {
165            NewUnstablePollStartEventContent::plain_text(text, content)
166        } else {
167            NewUnstablePollStartEventContent::new(content)
168        }
169    }
170}
171
172#[derive(Debug)]
173pub struct PollResult {
174    pub question: String,
175    pub kind: PollKind,
176    pub max_selections: u64,
177    pub answers: Vec<PollResultAnswer>,
178    pub votes: HashMap<String, Vec<String>>,
179    pub end_time: Option<MilliSecondsSinceUnixEpoch>,
180    pub has_been_edited: bool,
181}
182
183#[derive(Debug)]
184pub struct PollResultAnswer {
185    pub id: String,
186    pub text: String,
187}