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