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    /// Add a response to a poll.
87    pub(crate) fn add_response(
88        &mut self,
89        sender: OwnedUserId,
90        timestamp: MilliSecondsSinceUnixEpoch,
91        answers: Vec<String>,
92    ) {
93        self.response_data.push(ResponseData { sender, timestamp, answers });
94    }
95
96    /// Remove a response from the poll, as identified by its sender and
97    /// timestamp values.
98    pub(crate) fn remove_response(
99        &mut self,
100        sender: &UserId,
101        timestamp: MilliSecondsSinceUnixEpoch,
102    ) {
103        if let Some(idx) = self
104            .response_data
105            .iter()
106            .position(|resp| resp.sender == sender && resp.timestamp == timestamp)
107        {
108            self.response_data.remove(idx);
109        }
110    }
111
112    /// Marks the poll as ended.
113    ///
114    /// Returns false if the poll was already ended, true otherwise.
115    pub(crate) fn end(&mut self, timestamp: MilliSecondsSinceUnixEpoch) -> bool {
116        if self.end_event_timestamp.is_none() {
117            self.end_event_timestamp = Some(timestamp);
118            true
119        } else {
120            false
121        }
122    }
123
124    pub fn fallback_text(&self) -> Option<String> {
125        self.fallback_text.clone()
126    }
127
128    pub fn results(&self) -> PollResult {
129        let results = compile_unstable_poll_results(
130            &self.poll_start,
131            self.response_data.iter().map(|response_data| PollResponseData {
132                sender: &response_data.sender,
133                origin_server_ts: response_data.timestamp,
134                selections: &response_data.answers,
135            }),
136            self.end_event_timestamp,
137        );
138
139        PollResult {
140            question: self.poll_start.question.text.clone(),
141            kind: self.poll_start.kind.clone(),
142            max_selections: self.poll_start.max_selections.into(),
143            answers: self
144                .poll_start
145                .answers
146                .iter()
147                .map(|i| PollResultAnswer { id: i.id.clone(), text: i.text.clone() })
148                .collect(),
149            votes: results
150                .iter()
151                .map(|i| ((*i.0).to_owned(), i.1.iter().map(|i| i.to_string()).collect()))
152                .collect(),
153            end_time: self.end_event_timestamp,
154            has_been_edited: self.has_been_edited,
155        }
156    }
157
158    /// Returns true whether this poll has been edited.
159    pub fn is_edit(&self) -> bool {
160        self.has_been_edited
161    }
162}
163
164impl From<PollState> for NewUnstablePollStartEventContent {
165    fn from(value: PollState) -> Self {
166        let content = UnstablePollStartContentBlock::new(
167            value.poll_start.question.text.clone(),
168            value.poll_start.answers.clone(),
169        );
170        if let Some(text) = value.fallback_text() {
171            NewUnstablePollStartEventContent::plain_text(text, content)
172        } else {
173            NewUnstablePollStartEventContent::new(content)
174        }
175    }
176}
177
178#[derive(Debug)]
179pub struct PollResult {
180    pub question: String,
181    pub kind: PollKind,
182    pub max_selections: u64,
183    pub answers: Vec<PollResultAnswer>,
184    pub votes: HashMap<String, Vec<String>>,
185    pub end_time: Option<MilliSecondsSinceUnixEpoch>,
186    pub has_been_edited: bool,
187}
188
189#[derive(Debug)]
190pub struct PollResultAnswer {
191    pub id: String,
192    pub text: String,
193}