mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Added a pin icon (#3257)
This commit is contained in:
parent
ff5b22cecf
commit
ed67a29277
@ -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() {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPad-en-GB.Pinned-messages.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPad-en-GB.Pinned-messages.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPad-pseudo.Pinned-messages.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPad-pseudo.Pinned-messages.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPhone-15-en-GB.Pinned-messages.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPhone-15-en-GB.Pinned-messages.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPhone-15-pseudo.Pinned-messages.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView-iPhone-15-pseudo.Pinned-messages.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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>()
|
||||
|
Loading…
x
Reference in New Issue
Block a user