"View only" polls in the timeline (#1399)

* Add stub methods in RoomTimelineItemFactory

* Add PollRoomTimelineView and PollEndRoomTimelineView

* Add timeline skeleton

* Start poll mapping

* Add comment

* Refine poll UI

* Add feature flags for polls

* Cleanup

* Delete poll end event

* Refine poll mapping

* Refine poll layout

* Reuse FormRowAccessory

* Refine poll bubble layout

* Localise strings

* Update project

* Refine poll preview

* Map poll properties
This commit is contained in:
Alfonso Grillo
2023-07-27 15:34:27 +02:00
committed by GitHub
parent 8f2904701b
commit 0e7a48d961
19 changed files with 483 additions and 43 deletions
@@ -0,0 +1,50 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
struct PollRoomTimelineItem: Equatable, EventBasedTimelineItemProtocol {
let id: TimelineItemIdentifier
let poll: Poll
let body: String
let timestamp: String
let isOutgoing: Bool
let isEditable: Bool
let sender: TimelineItemSender
var properties: RoomTimelineItemProperties
}
struct Poll: Equatable {
let question: String
let pollKind: Kind
let maxSelections: Int
let options: [Option]
let votes: [String: [String]]
let endDate: Date?
enum Kind: Equatable {
case disclosed
case undisclosed
}
struct Option: Equatable {
let id: String
let text: String
let votes: Int
let allVotes: Int
let isSelected: Bool
}
}
@@ -57,28 +57,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
case .failedToParseState(let eventType, _, let error):
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing)
case .message:
guard let messageTimelineItem = eventItemProxy.content.asMessage() else { fatalError("Invalid message timeline item: \(eventItemProxy)") }
switch messageTimelineItem.msgtype() {
case .text(content: let content):
return buildTextTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .image(content: let content):
return buildImageTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .video(let content):
return buildVideoTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .file(let content):
return buildFileTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .notice(content: let content):
return buildNoticeTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .emote(content: let content):
return buildEmoteTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .audio(let content):
return buildAudioTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .location(let content):
return buildLocationTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .none:
return nil
}
return buildMessageTimelineItem(eventItemProxy, isOutgoing)
case .state(let stateKey, let content):
return buildStateTimelineItem(for: eventItemProxy, state: content, stateKey: stateKey, isOutgoing: isOutgoing)
case .roomMembership(userId: let userID, change: let change):
@@ -90,12 +69,44 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
avatarURLString: avatarUrl,
previousAvatarURLString: prevAvatarUrl,
isOutgoing: isOutgoing)
case .poll, .pollEnd:
case .poll(question: let question, kind: let kind, maxSelections: let maxSelections, answers: let answers, votes: let votes, endTime: let endTime):
guard ServiceLocator.shared.settings.pollsInTimelineEnabled else {
return nil
}
return buildPollTimelineItem(question, kind, maxSelections, answers, votes, endTime, eventItemProxy, isOutgoing)
case .pollEnd:
return nil
}
}
// MARK: - Message Events
private func buildMessageTimelineItem(_ eventItemProxy: EventTimelineItemProxy, _ isOutgoing: Bool) -> RoomTimelineItemProtocol? {
guard let messageTimelineItem = eventItemProxy.content.asMessage() else {
fatalError("Invalid message timeline item: \(eventItemProxy)")
}
switch messageTimelineItem.msgtype() {
case .text(content: let content):
return buildTextTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .image(content: let content):
return buildImageTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .video(let content):
return buildVideoTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .file(let content):
return buildFileTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .notice(content: let content):
return buildNoticeTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .emote(content: let content):
return buildEmoteTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .audio(let content):
return buildAudioTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .location(let content):
return buildLocationTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing)
case .none:
return nil
}
}
private func buildUnsupportedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ eventType: String,
@@ -309,6 +320,47 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
// swiftlint:disable:next function_parameter_count
private func buildPollTimelineItem(_ question: String,
_ pollKind: PollKind,
_ maxSelections: UInt64,
_ answers: [PollAnswer],
_ votes: [String: [String]],
_ endTime: UInt64?,
_ eventItemProxy: EventTimelineItemProxy,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
let allVotes = votes.reduce(0) { count, pair in
count + pair.value.count
}
let options = answers.map { answer in
Poll.Option(id: answer.id,
text: answer.text,
votes: votes[answer.id]?.count ?? 0,
allVotes: allVotes,
isSelected: votes[answer.id]?.contains(userID) ?? false)
}
let poll = Poll(question: question,
pollKind: .init(pollKind: pollKind),
maxSelections: Int(maxSelections),
options: options,
votes: votes,
endDate: endTime.map { Date(timeIntervalSince1970: TimeInterval($0 / 1000)) })
return PollRoomTimelineItem(id: eventItemProxy.id,
poll: poll,
body: poll.question,
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
sender: eventItemProxy.sender,
properties: RoomTimelineItemProperties(isEdited: false,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] {
reactions.map { reaction in
@@ -552,3 +604,14 @@ private extension LocationRoomTimelineItemContent.AssetType {
}
}
}
private extension Poll.Kind {
init(pollKind: MatrixRustSDK.PollKind) {
switch pollKind {
case .disclosed:
self = .disclosed
case .undisclosed:
self = .undisclosed
}
}
}
@@ -77,6 +77,8 @@ struct RoomTimelineItemView: View {
CollapsibleRoomTimelineView(timelineItem: item)
case .location(let item):
LocationRoomTimelineView(timelineItem: item)
case .poll(let item):
PollRoomTimelineView(timelineItem: item)
}
}
@@ -68,6 +68,7 @@ enum RoomTimelineItemType: Equatable {
case state(StateRoomTimelineItem)
case group(CollapsibleTimelineItem)
case location(LocationRoomTimelineItem)
case poll(PollRoomTimelineItem)
// swiftlint:disable:next cyclomatic_complexity
init(item: RoomTimelineItemProtocol) {
@@ -110,6 +111,8 @@ enum RoomTimelineItemType: Equatable {
self = .group(item)
case let item as LocationRoomTimelineItem:
self = .location(item)
case let item as PollRoomTimelineItem:
self = .poll(item)
default:
fatalError("Unknown timeline item")
}
@@ -135,7 +138,8 @@ enum RoomTimelineItemType: Equatable {
.encryptedHistory(let item as RoomTimelineItemProtocol),
.state(let item as RoomTimelineItemProtocol),
.group(let item as RoomTimelineItemProtocol),
.location(let item as RoomTimelineItemProtocol):
.location(let item as RoomTimelineItemProtocol),
.poll(let item as RoomTimelineItemProtocol):
return item.id
}
}
@@ -143,7 +147,7 @@ enum RoomTimelineItemType: Equatable {
/// Whether or not it is possible to send a reaction to this timeline item.
var isReactable: Bool {
switch self {
case .text, .image, .video, .audio, .file, .emote, .notice, .sticker, .location:
case .text, .image, .video, .audio, .file, .emote, .notice, .sticker, .location, .poll:
return true
case .redacted, .encrypted, .unsupported, .state: // Event based items that aren't reactable
return false