mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
"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:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "equalizer.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 4.000000 4.000000 cm
|
||||
0.196078 0.196078 0.196078 scn
|
||||
8.000000 0.000000 m
|
||||
9.100000 0.000000 10.000000 0.900000 10.000000 2.000000 c
|
||||
10.000000 14.000000 l
|
||||
10.000000 15.100000 9.100000 16.000000 8.000000 16.000000 c
|
||||
6.900000 16.000000 6.000000 15.100000 6.000000 14.000000 c
|
||||
6.000000 2.000000 l
|
||||
6.000000 0.900000 6.900000 0.000000 8.000000 0.000000 c
|
||||
h
|
||||
2.000000 0.000000 m
|
||||
3.100000 0.000000 4.000000 0.900000 4.000000 2.000000 c
|
||||
4.000000 6.000000 l
|
||||
4.000000 7.100000 3.100000 8.000000 2.000000 8.000000 c
|
||||
0.900000 8.000000 0.000000 7.100000 0.000000 6.000000 c
|
||||
0.000000 2.000000 l
|
||||
0.000000 0.900000 0.900000 0.000000 2.000000 0.000000 c
|
||||
h
|
||||
12.000000 9.000000 m
|
||||
12.000000 2.000000 l
|
||||
12.000000 0.900000 12.900000 0.000000 14.000000 0.000000 c
|
||||
15.100000 0.000000 16.000000 0.900000 16.000000 2.000000 c
|
||||
16.000000 9.000000 l
|
||||
16.000000 10.100000 15.100000 11.000000 14.000000 11.000000 c
|
||||
12.900000 11.000000 12.000000 10.100000 12.000000 9.000000 c
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
1014
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000001104 00000 n
|
||||
0000001127 00000 n
|
||||
0000001300 00000 n
|
||||
0000001374 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1433
|
||||
%%EOF
|
||||
@@ -18,6 +18,22 @@
|
||||
<string>%1$d members</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>common_poll_votes_count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>1 vote</string>
|
||||
<key>other</key>
|
||||
<string>%d votes</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>notification_compat_summary_line_for_room</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
|
||||
@@ -35,6 +35,7 @@ final class AppSettings {
|
||||
case hasShownWelcomeScreen
|
||||
case notificationSettingsEnabled
|
||||
case swiftUITimelineEnabled
|
||||
case pollsInTimeline
|
||||
}
|
||||
|
||||
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
||||
@@ -217,4 +218,7 @@ final class AppSettings {
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.swiftUITimelineEnabled, defaultValue: false, storageType: .volatile)
|
||||
var swiftUITimelineEnabled
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.pollsInTimeline, defaultValue: false, storageType: .userDefaults(store))
|
||||
var pollsInTimelineEnabled
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ internal enum Asset {
|
||||
internal static let locationPin = ImageAsset(name: "images/location-pin")
|
||||
internal static let locationPointerFull = ImageAsset(name: "images/location-pointer-full")
|
||||
internal static let locationPointer = ImageAsset(name: "images/location-pointer")
|
||||
internal static let equalizer = ImageAsset(name: "images/equalizer")
|
||||
internal static let timelineComposerSendMessage = ImageAsset(name: "images/timeline-composer-send-message")
|
||||
internal static let timelineReactionAddMore = ImageAsset(name: "images/timeline-reaction-add-more")
|
||||
internal static let waitingGradient = ImageAsset(name: "images/waiting-gradient")
|
||||
|
||||
@@ -208,6 +208,10 @@ public enum L10n {
|
||||
public static var commonPeople: String { return L10n.tr("Localizable", "common_people") }
|
||||
/// Permalink
|
||||
public static var commonPermalink: String { return L10n.tr("Localizable", "common_permalink") }
|
||||
/// Plural format key: "%#@COUNT@"
|
||||
public static func commonPollVotesCount(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "common_poll_votes_count", p1)
|
||||
}
|
||||
/// Privacy policy
|
||||
public static var commonPrivacyPolicy: String { return L10n.tr("Localizable", "common_privacy_policy") }
|
||||
/// Reactions
|
||||
|
||||
@@ -66,7 +66,7 @@ struct FormRowAccessory: View {
|
||||
}
|
||||
}
|
||||
|
||||
private init(kind: Kind) {
|
||||
init(kind: Kind) {
|
||||
self.kind = kind
|
||||
}
|
||||
}
|
||||
|
||||
+25
-15
@@ -196,8 +196,6 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
case .vertical:
|
||||
GridRow {
|
||||
localizedSendInfo
|
||||
.padding(.bottom, 4)
|
||||
.padding(.trailing, 4)
|
||||
.gridColumnAlignment(.trailing)
|
||||
}
|
||||
}
|
||||
@@ -284,7 +282,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
}
|
||||
|
||||
private extension View {
|
||||
func bubbleStyle(insets: CGFloat, color: Color? = nil, cornerRadius: CGFloat = 12, corners: UIRectCorner) -> some View {
|
||||
func bubbleStyle(insets: EdgeInsets, color: Color? = nil, cornerRadius: CGFloat = 12, corners: UIRectCorner) -> some View {
|
||||
padding(insets)
|
||||
.background(color)
|
||||
.cornerRadius(cornerRadius, corners: corners)
|
||||
@@ -293,18 +291,18 @@ private extension View {
|
||||
|
||||
// Describes how the content and the send info should be arranged inside a bubble
|
||||
private enum BubbleSendInfoLayoutType {
|
||||
case horizontal
|
||||
case vertical
|
||||
case horizontal(spacing: CGFloat = 4)
|
||||
case vertical(spacing: CGFloat = 4)
|
||||
case overlay(capsuleStyle: Bool)
|
||||
|
||||
var layout: AnyLayout {
|
||||
let layout: any Layout
|
||||
|
||||
switch self {
|
||||
case .horizontal:
|
||||
layout = HStackLayout(alignment: .bottom, spacing: 4)
|
||||
case .vertical:
|
||||
layout = GridLayout(alignment: .leading, verticalSpacing: 4)
|
||||
case .horizontal(let spacing):
|
||||
layout = HStackLayout(alignment: .bottom, spacing: spacing)
|
||||
case .vertical(let spacing):
|
||||
layout = GridLayout(alignment: .leading, verticalSpacing: spacing)
|
||||
case .overlay:
|
||||
layout = ZStackLayout(alignment: .bottomTrailing)
|
||||
}
|
||||
@@ -329,23 +327,25 @@ private extension EventBasedTimelineItemProtocol {
|
||||
|
||||
// The insets for the full bubble content.
|
||||
// Padding affecting just the "send info" should be added inside `layoutedLocalizedSendInfo`
|
||||
var bubbleInsets: CGFloat {
|
||||
let defaultPadding: CGFloat = 8
|
||||
var bubbleInsets: EdgeInsets {
|
||||
let defaultInsets: EdgeInsets = .init(around: 8)
|
||||
|
||||
switch self {
|
||||
case is ImageRoomTimelineItem,
|
||||
is VideoRoomTimelineItem,
|
||||
is StickerRoomTimelineItem:
|
||||
return 0
|
||||
return .zero
|
||||
case is PollRoomTimelineItem:
|
||||
return .init(top: 12, leading: 12, bottom: 4, trailing: 12)
|
||||
case let locationTimelineItem as LocationRoomTimelineItem:
|
||||
return locationTimelineItem.content.geoURI == nil ? defaultPadding : 0
|
||||
return locationTimelineItem.content.geoURI == nil ? defaultInsets : .zero
|
||||
default:
|
||||
return defaultPadding
|
||||
return defaultInsets
|
||||
}
|
||||
}
|
||||
|
||||
var bubbleSendInfoLayoutType: BubbleSendInfoLayoutType {
|
||||
let defaultTimestampLayout: BubbleSendInfoLayoutType = .horizontal
|
||||
let defaultTimestampLayout: BubbleSendInfoLayoutType = .horizontal()
|
||||
|
||||
switch self {
|
||||
case is TextBasedRoomTimelineItem:
|
||||
@@ -356,6 +356,8 @@ private extension EventBasedTimelineItemProtocol {
|
||||
return .overlay(capsuleStyle: true)
|
||||
case let locationTimelineItem as LocationRoomTimelineItem:
|
||||
return .overlay(capsuleStyle: locationTimelineItem.content.geoURI != nil)
|
||||
case is PollRoomTimelineItem:
|
||||
return .vertical(spacing: 16)
|
||||
default:
|
||||
return defaultTimestampLayout
|
||||
}
|
||||
@@ -415,3 +417,11 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
|
||||
.environmentObject(viewModel.context)
|
||||
}
|
||||
}
|
||||
|
||||
private extension EdgeInsets {
|
||||
init(around: CGFloat) {
|
||||
self.init(top: around, leading: around, bottom: around, trailing: around)
|
||||
}
|
||||
|
||||
static var zero: Self = .init(around: 0)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
struct PollOptionView: View {
|
||||
let pollOption: Poll.Option
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
FormRowAccessory(kind: .multipleSelection(isSelected: pollOption.isSelected))
|
||||
|
||||
VStack(spacing: 10) {
|
||||
HStack(alignment: .lastTextBaseline) {
|
||||
Text(pollOption.text)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Text(L10n.commonPollVotesCount(pollOption.votes))
|
||||
.font(.compound.bodySM)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
}
|
||||
|
||||
progressView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var progressView: some View {
|
||||
ProgressView(value: Double(pollOption.votes) / Double(pollOption.allVotes))
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: .compound.textPrimary))
|
||||
}
|
||||
}
|
||||
|
||||
struct PollOptionView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
Group {
|
||||
PollOptionView(pollOption: .init(id: "1",
|
||||
text: "Italian 🇮🇹",
|
||||
votes: 1,
|
||||
allVotes: 10,
|
||||
isSelected: true))
|
||||
|
||||
PollOptionView(pollOption: .init(id: "2",
|
||||
text: "Chinese 🇨🇳",
|
||||
votes: 9,
|
||||
allVotes: 10,
|
||||
isSelected: false))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
struct PollRoomTimelineView: View {
|
||||
let timelineItem: PollRoomTimelineItem
|
||||
@Environment(\.timelineStyle) var timelineStyle
|
||||
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
questionView
|
||||
|
||||
ForEach(poll.options, id: \.id) { option in
|
||||
Button { } label: {
|
||||
PollOptionView(pollOption: option)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 450)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var poll: Poll {
|
||||
timelineItem.poll
|
||||
}
|
||||
|
||||
private var questionView: some View {
|
||||
HStack(spacing: 4) {
|
||||
Image(Asset.Images.equalizer.name)
|
||||
|
||||
Text(poll.question)
|
||||
.font(.compound.bodyLGSemibold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PollRoomTimelineView_Previews: PreviewProvider {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
PollRoomTimelineView(timelineItem: .init(id: .random,
|
||||
poll: .mock,
|
||||
body: "Foo",
|
||||
timestamp: "Now",
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "Bob"),
|
||||
properties: .init()))
|
||||
.environment(\.timelineStyle, .bubbles)
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Poll bubble style")
|
||||
|
||||
PollRoomTimelineView(timelineItem: .init(id: .random,
|
||||
poll: .mock,
|
||||
body: "Foo",
|
||||
timestamp: "Now",
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "Bob"),
|
||||
properties: .init()))
|
||||
.environment(\.timelineStyle, .plain)
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Poll plain style")
|
||||
}
|
||||
}
|
||||
|
||||
private extension Poll {
|
||||
static let mock: Self = .init(question: "Do you like polls?",
|
||||
pollKind: .disclosed,
|
||||
maxSelections: 1,
|
||||
options: [.init(id: "1", text: "Yes", votes: 1, allVotes: 3, isSelected: true), .init(id: "2", text: "No", votes: 2, allVotes: 3, isSelected: false)],
|
||||
votes: [:],
|
||||
endDate: nil)
|
||||
}
|
||||
+1
@@ -48,6 +48,7 @@ protocol DeveloperOptionsProtocol: AnyObject {
|
||||
var readReceiptsEnabled: Bool { get set }
|
||||
var notificationSettingsEnabled: Bool { get set }
|
||||
var swiftUITimelineEnabled: Bool { get set }
|
||||
var pollsInTimelineEnabled: Bool { get set }
|
||||
}
|
||||
|
||||
extension AppSettings: DeveloperOptionsProtocol { }
|
||||
|
||||
+6
@@ -50,6 +50,12 @@ struct DeveloperOptionsScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section("Polls") {
|
||||
Toggle(isOn: $context.pollsInTimelineEnabled) {
|
||||
Text("View polls in timeline")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button {
|
||||
showConfetti = true
|
||||
|
||||
@@ -23,7 +23,7 @@ struct RoomEventStringBuilder {
|
||||
self.stateEventStringBuilder = stateEventStringBuilder
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
// swiftlint:disable:next cyclomatic_complexity function_body_length
|
||||
func buildAttributedString(for eventItemProxy: EventTimelineItemProxy) -> AttributedString? {
|
||||
let sender = eventItemProxy.sender
|
||||
let isOutgoing = eventItemProxy.isOwn
|
||||
@@ -77,6 +77,7 @@ struct RoomEventStringBuilder {
|
||||
memberIsYou: isOutgoing)
|
||||
.map(AttributedString.init)
|
||||
case .poll, .pollEnd:
|
||||
// The Rust SDK doesn't support poll events as room summaries yet
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
+50
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user