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(
54        content: NewUnstablePollStartEventContent,
55        edit: Option<NewUnstablePollStartEventContentWithoutRelation>,
56    ) -> Self {
57        let mut ret = Self {
58            start_event_content: content,
59            response_data: vec![],
60            end_event_timestamp: None,
61            has_been_edited: false,
62        };
63
64        if let Some(edit) = edit {
65            // SAFETY: [`Self::edit`] only returns `None` when the poll has ended, not the
66            // case here.
67            ret = ret.edit(edit).unwrap();
68        }
69
70        ret
71    }
72
73    /// Applies an edit to a poll, returns `None` if the poll was already marked
74    /// as finished.
75    pub(crate) fn edit(
76        &self,
77        replacement: NewUnstablePollStartEventContentWithoutRelation,
78    ) -> Option<Self> {
79        if self.end_event_timestamp.is_none() {
80            let mut clone = self.clone();
81            clone.start_event_content.poll_start = replacement.poll_start;
82            clone.start_event_content.text = replacement.text;
83            clone.has_been_edited = true;
84            Some(clone)
85        } else {
86            None
87        }
88    }
89
90    /// Add a response to a poll.
91    pub(crate) fn add_response(
92        &mut self,
93        sender: OwnedUserId,
94        timestamp: MilliSecondsSinceUnixEpoch,
95        answers: Vec<String>,
96    ) {
97        self.response_data.push(ResponseData { sender, timestamp, answers });
98    }
99
100    /// Remove a response from the poll, as identified by its sender and
101    /// timestamp values.
102    pub(crate) fn remove_response(
103        &mut self,
104        sender: &UserId,
105        timestamp: MilliSecondsSinceUnixEpoch,
106    ) {
107        if let Some(idx) = self
108            .response_data
109            .iter()
110            .position(|resp| resp.sender == sender && resp.timestamp == timestamp)
111        {
112            self.response_data.remove(idx);
113        }
114    }
115
116    /// Marks the poll as ended.
117    ///
118    /// Returns false if the poll was already ended, true otherwise.
119    pub(crate) fn end(&mut self, timestamp: MilliSecondsSinceUnixEpoch) -> bool {
120        if self.end_event_timestamp.is_none() {
121            self.end_event_timestamp = Some(timestamp);
122            true
123        } else {
124            false
125        }
126    }
127
128    pub fn fallback_text(&self) -> Option<String> {
129        self.start_event_content.text.clone()
130    }
131
132    pub fn results(&self) -> PollResult {
133        let results = compile_unstable_poll_results(
134            &self.start_event_content.poll_start,
135            self.response_data.iter().map(|response_data| PollResponseData {
136                sender: &response_data.sender,
137                origin_server_ts: response_data.timestamp,
138                selections: &response_data.answers,
139            }),
140            self.end_event_timestamp,
141        );
142
143        PollResult {
144            question: self.start_event_content.poll_start.question.text.clone(),
145            kind: self.start_event_content.poll_start.kind.clone(),
146            max_selections: self.start_event_content.poll_start.max_selections.into(),
147            answers: self
148                .start_event_content
149                .poll_start
150                .answers
151                .iter()
152                .map(|i| PollResultAnswer { id: i.id.clone(), text: i.text.clone() })
153                .collect(),
154            votes: results
155                .iter()
156                .map(|i| ((*i.0).to_owned(), i.1.iter().map(|i| i.to_string()).collect()))
157                .collect(),
158            end_time: self.end_event_timestamp,
159            has_been_edited: self.has_been_edited,
160        }
161    }
162
163    /// Returns true whether this poll has been edited.
164    pub fn is_edit(&self) -> bool {
165        self.has_been_edited
166    }
167}
168
169impl From<PollState> for NewUnstablePollStartEventContent {
170    fn from(value: PollState) -> Self {
171        let content = UnstablePollStartContentBlock::new(
172            value.start_event_content.poll_start.question.text.clone(),
173            value.start_event_content.poll_start.answers.clone(),
174        );
175        if let Some(text) = value.fallback_text() {
176            NewUnstablePollStartEventContent::plain_text(text, content)
177        } else {
178            NewUnstablePollStartEventContent::new(content)
179        }
180    }
181}
182
183#[derive(Debug)]
184pub struct PollResult {
185    pub question: String,
186    pub kind: PollKind,
187    pub max_selections: u64,
188    pub answers: Vec<PollResultAnswer>,
189    pub votes: HashMap<String, Vec<String>>,
190    pub end_time: Option<MilliSecondsSinceUnixEpoch>,
191    pub has_been_edited: bool,
192}
193
194#[derive(Debug)]
195pub struct PollResultAnswer {
196    pub id: String,
197    pub text: String,
198}