Added a pin icon (#3257)

This commit is contained in:
Mauro 2024-09-11 12:40:11 +02:00 committed by GitHub
parent ff5b22cecf
commit ed67a29277
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 168 additions and 2 deletions

View File

@ -440,7 +440,9 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
}
private func updatePinnedEventIDs() async {
state.pinnedEventIDs = await roomProxy.pinnedEventIDs
if appSettings.pinningEnabled {
state.pinnedEventIDs = await roomProxy.pinnedEventIDs
}
}
private func setupDirectRoomSubscriptionsIfNeeded() {

View File

@ -19,6 +19,13 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
private var isEncryptedOneToOneRoom: Bool { context.viewState.isEncryptedOneToOneRoom }
private var isFocussed: Bool { focussedEventID != nil && timelineItem.id.eventID == focussedEventID }
private var isPinned: Bool {
guard !context.viewState.isPinnedEventsTimeline,
let eventID = timelineItem.id.eventID else {
return false
}
return context.viewState.pinnedEventIDs.contains(eventID)
}
/// The base padding applied to bubbles on either side.
///
@ -146,6 +153,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
context.send(viewAction: .handleTimelineItemMenuAction(itemID: timelineItem.id, action: action))
}
}
.pinnedIndicator(isPinned: isPinned, isOutgoing: timelineItem.isOutgoing)
.padding(.top, messageBubbleTopPadding)
}
@ -309,10 +317,58 @@ private extension EdgeInsets {
static var zero: Self = .init(around: 0)
}
private struct PinnedIndicatorViewModifier: ViewModifier {
let isPinned: Bool
let isOutgoing: Bool
func body(content: Content) -> some View {
if isPinned {
HStack(alignment: .top, spacing: 8) {
if isOutgoing {
pinnedIndicator
}
content
.layoutPriority(1)
if !isOutgoing {
pinnedIndicator
}
}
} else {
content
}
}
private var pinnedIndicator: some View {
CompoundIcon(\.pinSolid, size: .xSmall, relativeTo: .compound.bodyMD)
.foregroundStyle(Color.compound.iconTertiary)
}
}
private extension View {
func pinnedIndicator(isPinned: Bool, isOutgoing: Bool) -> some View {
modifier(PinnedIndicatorViewModifier(isPinned: isPinned, isOutgoing: isOutgoing))
}
}
// MARK: - Previews
struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview {
static let viewModel = TimelineViewModel.mock
static let viewModelWithPins: TimelineViewModel = {
var settings = AppSettings()
settings.pinningEnabled = true
let roomProxy = JoinedRoomProxyMock(.init(name: "Preview Room", pinnedEventIDs: [""]))
return TimelineViewModel(roomProxy: roomProxy,
focussedEventID: nil,
timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: AppMediatorMock.default,
appSettings: settings,
analyticsService: ServiceLocator.shared.analytics)
}()
static var previews: some View {
mockTimeline
@ -326,9 +382,12 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
.previewDisplayName("Thread decorator")
encryptionAuthenticity
.previewDisplayName("Encryption Indicators")
pinned
.previewDisplayName("Pinned messages")
.snapshotPreferences(delay: 1.0)
}
// These akwats include a reply
// These always include a reply
static var threads: some View {
ScrollView {
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""),
@ -555,4 +614,96 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
}
.environmentObject(viewModel.context)
}
static var pinned: some View {
ScrollView {
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: "", eventID: ""),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."),
replyDetails: nil),
groupStyle: .single))
AudioRoomTimelineView(timelineItem: .init(id: .init(timelineID: "", eventID: ""),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: ""),
content: .init(body: "audio.ogg",
duration: 100,
waveform: EstimatedWaveform.mockWaveform,
source: nil,
contentType: nil),
replyDetails: nil))
FileRoomTimelineView(timelineItem: .init(id: .init(timelineID: "", eventID: ""),
timestamp: "10:42",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: ""),
content: .init(body: "File",
source: nil,
thumbnailSource: nil,
contentType: nil),
replyDetails: nil))
ImageRoomTimelineView(timelineItem: .init(id: .init(timelineID: "", eventID: ""),
timestamp: "10:42",
isOutgoing: true,
isEditable: true,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: ""),
content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil),
replyDetails: nil))
LocationRoomTimelineView(timelineItem: .init(id: .init(timelineID: "", eventID: ""),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782,
longitude: 12.496366),
description: "Location description description description description description description description description"),
replyDetails: nil))
LocationRoomTimelineView(timelineItem: .init(id: .init(timelineID: "", eventID: ""),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil),
replyDetails: nil))
VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(timelineID: "", eventID: ""),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: ""),
content: .init(body: "audio.ogg",
duration: 100,
waveform: EstimatedWaveform.mockWaveform,
source: nil,
contentType: nil),
replyDetails: nil),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.random),
title: L10n.commonVoiceMessage,
duration: 10,
waveform: EstimatedWaveform.mockWaveform))
}
.environmentObject(viewModelWithPins.context)
}
}

View File

@ -379,6 +379,7 @@ class TimelineViewModelTests: XCTestCase {
// MARK: - Pins
func testPinnedEvents() async throws {
ServiceLocator.shared.settings.pinningEnabled = true
let roomProxyMock = JoinedRoomProxyMock(.init(name: "",
pinnedEventIDs: .init(["test1"])))
let actionsSubject = PassthroughSubject<JoinedRoomProxyAction, Never>()