mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Run SwiftFormat.
This commit is contained in:
@@ -36,14 +36,14 @@ struct TimelineItemMenu: View {
|
||||
if !actions.reactions.isEmpty {
|
||||
reactionsSection
|
||||
.padding(.bottom, 8.0)
|
||||
|
||||
|
||||
Divider()
|
||||
.background(Color.compound.bgSubtlePrimary)
|
||||
}
|
||||
|
||||
|
||||
if !actions.actions.isEmpty {
|
||||
viewsForActions(actions.actions)
|
||||
|
||||
|
||||
Divider()
|
||||
.background(Color.compound.bgSubtlePrimary)
|
||||
}
|
||||
@@ -278,7 +278,7 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
static let (mediaItem, mediaItemActions) = makeActions(itemType: .outgoingMedia)
|
||||
static let (mediaItemWithCaption, mediaItemWithCaptionActions) = makeActions(itemType: .outgoingMediaWithCaption)
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
TimelineItemMenu(item: item, actions: actions)
|
||||
.environmentObject(viewModel.context)
|
||||
|
||||
+7
-7
@@ -28,12 +28,12 @@ struct TimelineItemMenuActionProvider {
|
||||
// Don't show a context menu for non-event based items.
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
if timelineItem is StateRoomTimelineItem {
|
||||
// Don't show a context menu for state events.
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
if let encryptedItem = timelineItem as? EncryptedRoomTimelineItem {
|
||||
return makeEncryptedItemActions(encryptedItem)
|
||||
}
|
||||
@@ -48,7 +48,7 @@ struct TimelineItemMenuActionProvider {
|
||||
if canRedactItem(item), let poll = item.pollIfAvailable, !poll.hasEnded, let eventID = item.id.eventID {
|
||||
actions.append(.endPoll(pollStartID: eventID))
|
||||
}
|
||||
|
||||
|
||||
if item.canBeRepliedTo, canCurrentUserSendMessage {
|
||||
if let messageItem = item as? EventBasedMessageTimelineItemProtocol {
|
||||
// If threads are enabled we will have the dedicated `replyInThread` action
|
||||
@@ -88,7 +88,7 @@ struct TimelineItemMenuActionProvider {
|
||||
if canCurrentUserPin, let eventID = item.id.eventID {
|
||||
actions.append(pinnedEventIDs.contains(eventID) ? .unpin : .pin)
|
||||
}
|
||||
|
||||
|
||||
if item.isCopyable {
|
||||
actions.append(.copy)
|
||||
|
||||
@@ -141,17 +141,17 @@ struct TimelineItemMenuActionProvider {
|
||||
}
|
||||
|
||||
let isReactable = timelineKind == .live || timelineKind == .detached || timelineKind.isThread ? item.isReactable : false
|
||||
|
||||
|
||||
return .init(isReactable: isReactable, actions: actions, secondaryActions: secondaryActions, emojiProvider: emojiProvider)
|
||||
}
|
||||
|
||||
private func makeEncryptedItemActions(_ encryptedItem: EncryptedRoomTimelineItem) -> TimelineItemMenuActions? {
|
||||
var actions: [TimelineItemMenuAction] = [.copyPermalink]
|
||||
|
||||
|
||||
if isViewSourceEnabled {
|
||||
actions.append(.viewSource)
|
||||
}
|
||||
|
||||
|
||||
return .init(isReactable: false,
|
||||
actions: actions,
|
||||
secondaryActions: [],
|
||||
|
||||
@@ -15,14 +15,14 @@ struct PollOptionView: View {
|
||||
let pollOption: Poll.Option
|
||||
let showVotes: Bool
|
||||
let isFinalResult: Bool
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 12) {
|
||||
Image(systemSymbol: pollOption.isSelected ? .checkmarkCircleFill : .circle)
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundColor(pollOption.isSelected && isEnabled ? .compound.iconPrimary : .compound.iconTertiary)
|
||||
.accessibilityAddTraits(pollOption.isSelected ? .isSelected : [])
|
||||
|
||||
|
||||
VStack(spacing: 10) {
|
||||
HStack(alignment: .lastTextBaseline, spacing: 8) {
|
||||
Text(pollOption.text)
|
||||
@@ -30,7 +30,7 @@ struct PollOptionView: View {
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
|
||||
if showVotes {
|
||||
if isFinalWinningOption {
|
||||
HStack(spacing: 4) {
|
||||
@@ -49,14 +49,14 @@ struct PollOptionView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
PollProgressView(progress: progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
private var progress: Double {
|
||||
switch (showVotes, pollOption.allVotes, pollOption.isSelected) {
|
||||
case (true, let allVotes, _) where allVotes > 0:
|
||||
@@ -67,7 +67,7 @@ struct PollOptionView: View {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var isFinalWinningOption: Bool {
|
||||
pollOption.isWinning && isFinalResult
|
||||
}
|
||||
@@ -75,13 +75,13 @@ struct PollOptionView: View {
|
||||
|
||||
private struct PollProgressView: View {
|
||||
let progress: Double
|
||||
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
Capsule()
|
||||
.foregroundColor(.compound._bgEmptyItemAlpha)
|
||||
|
||||
|
||||
Capsule()
|
||||
.frame(maxWidth: progress * geometry.size.width)
|
||||
}
|
||||
@@ -102,7 +102,7 @@ struct PollOptionView_Previews: PreviewProvider, TestablePreview {
|
||||
isWinning: false),
|
||||
showVotes: false,
|
||||
isFinalResult: false)
|
||||
|
||||
|
||||
PollOptionView(pollOption: .init(id: "2",
|
||||
text: "Chinese 🇨🇳",
|
||||
votes: 9,
|
||||
@@ -111,7 +111,7 @@ struct PollOptionView_Previews: PreviewProvider, TestablePreview {
|
||||
isWinning: true),
|
||||
showVotes: true,
|
||||
isFinalResult: false)
|
||||
|
||||
|
||||
PollOptionView(pollOption: .init(id: "2",
|
||||
text: "Chinese 🇨🇳",
|
||||
votes: 9,
|
||||
|
||||
@@ -59,7 +59,7 @@ struct PollView: View {
|
||||
.frame(maxWidth: 450)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var senderString: String {
|
||||
@@ -76,14 +76,14 @@ struct PollView: View {
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var questionView: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
CompoundIcon(poll.hasEnded ? \.pollsEnd : \.polls,
|
||||
size: .custom(22),
|
||||
relativeTo: .compound.bodyLGSemibold)
|
||||
.accessibilityLabel(poll.hasEnded ? L10n.a11yPollEnd : L10n.a11yPoll)
|
||||
|
||||
|
||||
Text(poll.question)
|
||||
.multilineTextAlignment(.leading)
|
||||
.font(.compound.bodyLGSemibold)
|
||||
@@ -91,7 +91,7 @@ struct PollView: View {
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityAddTraits(.isHeader)
|
||||
}
|
||||
|
||||
|
||||
private var optionsView: some View {
|
||||
ForEach(poll.options, id: \.id) { option in
|
||||
pollOption(option: option)
|
||||
@@ -116,7 +116,7 @@ struct PollView: View {
|
||||
}
|
||||
.disabled(poll.hasEnded)
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var summaryView: some View {
|
||||
if let summaryText = poll.summaryText {
|
||||
@@ -127,7 +127,7 @@ struct PollView: View {
|
||||
.frame(maxWidth: .infinity, alignment: showVotes ? .trailing : .leading)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var toolbarView: some View {
|
||||
if !poll.hasEnded, poll.createdByAccountOwner {
|
||||
@@ -157,7 +157,7 @@ struct PollView: View {
|
||||
actionHandler(.end)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func progressBarColor(for option: Poll.Option) -> Color {
|
||||
if poll.hasEnded {
|
||||
return option.isWinning ? .compound.textActionAccent : .compound.textDisabled
|
||||
@@ -165,7 +165,7 @@ struct PollView: View {
|
||||
return .compound.textPrimary
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var showVotes: Bool {
|
||||
poll.hasEnded || poll.kind == .disclosed
|
||||
}
|
||||
@@ -178,7 +178,7 @@ private extension Poll {
|
||||
L10n.commonPollTotalVotes($0.allVotes)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
switch kind {
|
||||
case .disclosed:
|
||||
return options.first.map {
|
||||
@@ -195,19 +195,19 @@ struct PollView_Previews: PreviewProvider, TestablePreview {
|
||||
PollView(poll: .disclosed(), state: .full(isEditable: false), sender: .test) { _ in }
|
||||
.padding()
|
||||
.previewDisplayName("Disclosed")
|
||||
|
||||
|
||||
PollView(poll: .undisclosed(), state: .full(isEditable: false), sender: .test) { _ in }
|
||||
.padding()
|
||||
.previewDisplayName("Undisclosed")
|
||||
|
||||
|
||||
PollView(poll: .endedDisclosed, state: .full(isEditable: false), sender: .test) { _ in }
|
||||
.padding()
|
||||
.previewDisplayName("Ended, Disclosed")
|
||||
|
||||
|
||||
PollView(poll: .endedUndisclosed, state: .full(isEditable: false), sender: .test) { _ in }
|
||||
.padding()
|
||||
.previewDisplayName("Ended, Undisclosed")
|
||||
|
||||
|
||||
PollView(poll: .disclosed(createdByAccountOwner: true), state: .full(isEditable: true), sender: .test) { _ in }
|
||||
.padding()
|
||||
.previewDisplayName("Creator, disclosed")
|
||||
|
||||
@@ -23,7 +23,7 @@ struct ReadReceiptCell: View {
|
||||
}
|
||||
return readReceipt.userID
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
LoadableAvatarImage(url: memberState?.avatarURL,
|
||||
|
||||
@@ -44,9 +44,9 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview {
|
||||
.mockDan
|
||||
]
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: "Room", members: members))
|
||||
|
||||
|
||||
let appSettings = AppSettings.volatile()
|
||||
|
||||
|
||||
return TimelineViewModel(roomProxy: roomProxyMock,
|
||||
timelineController: MockTimelineController(),
|
||||
userSession: UserSessionMock(.init()),
|
||||
|
||||
@@ -144,7 +144,7 @@ private struct LongPressGestureRepresentable: UIGestureRecognizerRepresentable {
|
||||
func handleUIGestureRecognizerAction(_ recognizer: UILongPressGestureRecognizer, context: Context) {
|
||||
handle(recognizer)
|
||||
}
|
||||
|
||||
|
||||
class Coordinator: NSObject, UIGestureRecognizerDelegate {
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
||||
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
|
||||
@@ -69,7 +69,7 @@ struct TimelineBubbleLayout: Layout {
|
||||
|
||||
// Calculate the natural size using the regular text and non-greedy quote bubbles.
|
||||
let layoutSubviews = subviews.filter { $0[Size.self].shouldLayout }
|
||||
|
||||
|
||||
let subviewSizes = layoutSubviews.map { size(for: $0, subviews: subviews, proposedSize: proposal, cache: &cache) }
|
||||
|
||||
let maxWidth = subviewSizes.map(\.width).reduce(0, max)
|
||||
@@ -88,7 +88,7 @@ struct TimelineBubbleLayout: Layout {
|
||||
|
||||
// Place the regular text and greedy components using the calculated width.
|
||||
let visibleSubviews = subviews.filter { $0[Size.self].shouldRender }
|
||||
|
||||
|
||||
let subviewSizes = visibleSubviews.map { size(for: $0, subviews: subviews, proposedSize: ProposedViewSize(width: maxWidth, height: proposal.height), cache: &cache) }
|
||||
|
||||
var y = bounds.minY
|
||||
|
||||
@@ -17,15 +17,15 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
let adjustedDeliveryStatus: TimelineItemDeliveryStatus?
|
||||
@ViewBuilder let content: () -> Content
|
||||
|
||||
|
||||
private var isDM: Bool {
|
||||
context.viewState.isDM
|
||||
}
|
||||
|
||||
|
||||
private var isFocussed: Bool {
|
||||
focussedEventID != nil && timelineItem.id.eventID == focussedEventID
|
||||
}
|
||||
|
||||
|
||||
private var isPinned: Bool {
|
||||
guard context.viewState.timelineKind != .pinned,
|
||||
let eventID = timelineItem.id.eventID else {
|
||||
@@ -51,17 +51,17 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
header
|
||||
.zIndex(1)
|
||||
}
|
||||
|
||||
|
||||
VStack(alignment: alignment, spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
if timelineItem.isOutgoing {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
|
||||
messageBubbleWithReactions
|
||||
}
|
||||
.padding(timelineItem.isOutgoing ? .leading : .trailing, 48) // Additional padding to differentiate alignment.
|
||||
|
||||
|
||||
HStack(spacing: 0) {
|
||||
if !timelineItem.isOutgoing {
|
||||
Spacer()
|
||||
@@ -246,12 +246,12 @@ private extension EventBasedTimelineItemProtocol {
|
||||
return defaultColor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The insets for the full bubble content.
|
||||
/// Padding affecting just the "send info" should be added inside `TimelineItemSendInfoView`
|
||||
var bubbleInsets: EdgeInsets {
|
||||
let defaultInsets: EdgeInsets = .init(around: 8)
|
||||
|
||||
|
||||
switch self {
|
||||
case is StickerRoomTimelineItem:
|
||||
return .zero
|
||||
@@ -286,7 +286,7 @@ private extension EdgeInsets {
|
||||
init(around: CGFloat) {
|
||||
self.init(top: around, leading: around, bottom: around, trailing: around)
|
||||
}
|
||||
|
||||
|
||||
static var zero: Self = .init(around: 0)
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
linkMetadataProvider: LinkMetadataProvider(),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
}()
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
mockTimeline
|
||||
.previewDisplayName("Mock Timeline")
|
||||
@@ -465,7 +465,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
.environmentObject(viewModelWithPins.context)
|
||||
.environment(\.timelineContext, viewModel.context)
|
||||
}
|
||||
|
||||
|
||||
static var pinned: some View {
|
||||
ScrollView {
|
||||
MockTimelineContent(isPinned: true)
|
||||
@@ -646,7 +646,7 @@ private struct MockTimelineContent: View {
|
||||
isThreaded: isThreaded,
|
||||
threadSummary: threadSummary)),
|
||||
groupStyle: .single))
|
||||
|
||||
|
||||
AudioRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
|
||||
@@ -14,7 +14,7 @@ enum TimelineGroupStyle: Hashable {
|
||||
case first
|
||||
case middle
|
||||
case last
|
||||
|
||||
|
||||
var shouldShowSenderDetails: Bool {
|
||||
switch self {
|
||||
case .single, .first:
|
||||
|
||||
@@ -23,7 +23,7 @@ struct TimelineStyler<Content: View>: View {
|
||||
self.content = content
|
||||
_adjustedDeliveryStatus = State(initialValue: timelineItem.properties.deliveryStatus)
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
mainContent
|
||||
.onChange(of: timelineItem.properties.deliveryStatus) { _, newStatus in
|
||||
@@ -56,7 +56,7 @@ struct TimelineStyler<Content: View>: View {
|
||||
|
||||
struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = TimelineViewModel.mock
|
||||
|
||||
|
||||
static let base = TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
@@ -64,19 +64,19 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
canBeRepliedTo: true,
|
||||
sender: .test,
|
||||
content: .init(body: "Test"))
|
||||
|
||||
|
||||
static let sentNonLast: TextRoomTimelineItem = {
|
||||
var result = base
|
||||
result.properties.deliveryStatus = .sent
|
||||
return result
|
||||
}()
|
||||
|
||||
|
||||
static let sendingNonLast: TextRoomTimelineItem = {
|
||||
var result = base
|
||||
result.properties.deliveryStatus = .sending
|
||||
return result
|
||||
}()
|
||||
|
||||
|
||||
static let sendingLast: TextRoomTimelineItem = {
|
||||
let id = viewModel.state.timelineState.uniqueIDs.last ?? .init(UUID().uuidString)
|
||||
var result = TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventID(UUID().uuidString)),
|
||||
@@ -89,13 +89,13 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
result.properties.deliveryStatus = .sending
|
||||
return result
|
||||
}()
|
||||
|
||||
|
||||
static let failed: TextRoomTimelineItem = {
|
||||
var result = base
|
||||
result.properties.deliveryStatus = .sendingFailed(.unknown)
|
||||
return result
|
||||
}()
|
||||
|
||||
|
||||
static let sentLast: TextRoomTimelineItem = {
|
||||
let id = viewModel.state.timelineState.uniqueIDs.last ?? .init(UUID().uuidString)
|
||||
return TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventID(UUID().uuidString)),
|
||||
@@ -106,21 +106,21 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
sender: .test,
|
||||
content: .init(body: "Test"))
|
||||
}()
|
||||
|
||||
|
||||
static let ltrString = TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
sender: .test, content: .init(body: "house!"))
|
||||
|
||||
|
||||
static let rtlString = TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
sender: .test, content: .init(body: "באמת!"))
|
||||
|
||||
|
||||
static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
@@ -128,7 +128,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
canBeRepliedTo: true,
|
||||
sender: .test,
|
||||
content: .init(body: "house! -- באמת! -- house!"))
|
||||
|
||||
|
||||
static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
@@ -136,7 +136,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
canBeRepliedTo: true,
|
||||
sender: .test,
|
||||
content: .init(body: "באמת! -- house! -- באמת!"))
|
||||
|
||||
|
||||
static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
@@ -144,7 +144,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
canBeRepliedTo: true,
|
||||
sender: .test,
|
||||
content: .init(body: "house! -- באמת!"))
|
||||
|
||||
|
||||
static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
@@ -161,7 +161,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
shouldBoost: true,
|
||||
sender: .test,
|
||||
content: .init(body: "😮"))
|
||||
|
||||
|
||||
static let endingWithBlockquote: TextRoomTimelineItem = {
|
||||
let builder = AttributedStringBuilder(cacheKey: "preview", mentionBuilder: MentionBuilder())
|
||||
let attributedString = builder.fromHTML("<p>Some text before</p><blockquote>A quoted line at the end</blockquote>")
|
||||
@@ -173,7 +173,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
sender: .test,
|
||||
content: .init(body: "", formattedBody: attributedString))
|
||||
}()
|
||||
|
||||
|
||||
static let endingWithCodeblock: TextRoomTimelineItem = {
|
||||
let builder = AttributedStringBuilder(cacheKey: "preview", mentionBuilder: MentionBuilder())
|
||||
let attributedString = builder.fromHTML("<p>Some text before</p><pre><code>let x = 42</code></pre>")
|
||||
@@ -185,7 +185,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
sender: .test,
|
||||
content: .init(body: "", formattedBody: attributedString))
|
||||
}()
|
||||
|
||||
|
||||
static var testView: some View {
|
||||
VStack(spacing: 0) {
|
||||
TextRoomTimelineView(timelineItem: base)
|
||||
@@ -198,7 +198,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
TextRoomTimelineView(timelineItem: endingWithCodeblock)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static var languagesTestView: some View {
|
||||
VStack(spacing: 0) {
|
||||
TextRoomTimelineView(timelineItem: ltrString)
|
||||
@@ -210,7 +210,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
TextRoomTimelineView(timelineItem: bigEmoji)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
testView
|
||||
.environmentObject(viewModel.context)
|
||||
@@ -219,7 +219,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
languagesTestView
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Bubbles LTR with different layout languages")
|
||||
|
||||
|
||||
languagesTestView
|
||||
.environmentObject(viewModel.context)
|
||||
.environment(\.layoutDirection, .rightToLeft)
|
||||
|
||||
+2
-2
@@ -14,9 +14,9 @@ struct TimelineDeliveryStatusView: View {
|
||||
case sending
|
||||
case sent
|
||||
}
|
||||
|
||||
|
||||
let deliveryStatus: Status
|
||||
|
||||
|
||||
private var icon: CompoundIcon {
|
||||
switch deliveryStatus {
|
||||
case .sending:
|
||||
|
||||
@@ -13,15 +13,15 @@ struct TimelineItemStatusView: View {
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
let adjustedDeliveryStatus: TimelineItemDeliveryStatus?
|
||||
@EnvironmentObject private var context: TimelineViewModel.Context
|
||||
|
||||
|
||||
private var isLastOutgoingMessage: Bool {
|
||||
timelineItem.isOutgoing && context.viewState.timelineState.uniqueIDs.last == timelineItem.id.uniqueID
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
mainContent
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var mainContent: some View {
|
||||
if context.viewState.timelineKind == .pinned {
|
||||
@@ -33,7 +33,7 @@ struct TimelineItemStatusView: View {
|
||||
deliveryStatusBadge
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
var deliveryStatusBadge: some View {
|
||||
switch adjustedDeliveryStatus {
|
||||
@@ -49,7 +49,7 @@ struct TimelineItemStatusView: View {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var readReceipts: some View {
|
||||
TimelineReadReceiptsView(timelineItem: timelineItem)
|
||||
.environmentObject(context)
|
||||
|
||||
@@ -13,7 +13,7 @@ import SwiftUI
|
||||
struct TimelineReactionsView: View {
|
||||
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
||||
@Environment(\.layoutDirection) private var layoutDirection: LayoutDirection
|
||||
|
||||
|
||||
let context: TimelineViewModel.Context
|
||||
let itemID: TimelineItemIdentifier
|
||||
let reactions: [AggregatedReaction]
|
||||
|
||||
@@ -20,7 +20,7 @@ struct TimelineReadReceiptsView: View {
|
||||
contentID: receipt.userID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 2) {
|
||||
StackedAvatarsView(overlap: 6,
|
||||
@@ -78,10 +78,10 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
|
||||
.mockDan,
|
||||
.mockMe
|
||||
]
|
||||
|
||||
|
||||
static let viewModel = {
|
||||
let appSettings = AppSettings.volatile()
|
||||
|
||||
|
||||
return TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Test", members: members)),
|
||||
timelineController: MockTimelineController(),
|
||||
userSession: UserSessionMock(.init()),
|
||||
@@ -94,7 +94,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
|
||||
linkMetadataProvider: LinkMetadataProvider(),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
}()
|
||||
|
||||
|
||||
static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")]
|
||||
static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"),
|
||||
ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before")]
|
||||
@@ -105,7 +105,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
|
||||
ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before"),
|
||||
ReadReceipt(userID: RoomMemberProxyMock.mockCharlie.userID, formattedTimestamp: "Way before"),
|
||||
ReadReceipt(userID: RoomMemberProxyMock.mockDan.userID, formattedTimestamp: "Way, way before")]
|
||||
|
||||
|
||||
static func mockTimelineItem(with receipts: [ReadReceipt]) -> TextRoomTimelineItem {
|
||||
TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
@@ -115,7 +115,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
|
||||
sender: .init(id: UUID().uuidString), content: .init(body: "Test"),
|
||||
properties: .init(orderedReadReceipts: receipts))
|
||||
}
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
VStack(spacing: 8) {
|
||||
TimelineReadReceiptsView(timelineItem: mockTimelineItem(with: singleReceipt))
|
||||
|
||||
+18
-18
@@ -20,14 +20,14 @@ struct CallNotificationRoomTimelineView: View {
|
||||
CompoundIcon(iconKeyPath, size: .medium, relativeTo: .compound.headingMDBold)
|
||||
.foregroundStyle(.compound.iconSecondary)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
|
||||
Text(tileTitle)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.labelStyle(.custom(spacing: 4))
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
Text(timelineItem.timestamp.formattedTime())
|
||||
.font(.compound.bodyXS)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
@@ -39,7 +39,7 @@ struct CallNotificationRoomTimelineView: View {
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
private var tileTitle: String {
|
||||
if timelineItem.isDM {
|
||||
// As per design only have declined variants in DM
|
||||
@@ -54,7 +54,7 @@ struct CallNotificationRoomTimelineView: View {
|
||||
L10n.commonCallStarted
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var iconKeyPath: KeyPath<CompoundIcons, Image> {
|
||||
if timelineItem.isDM, timelineItem.isDeclined || timelineItem.isDeclinedByMe {
|
||||
// As per design only have declined variants in DM
|
||||
@@ -82,9 +82,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
|
||||
isDeclinedByMe: false,
|
||||
isDeclined: false,
|
||||
isVoiceCall: false))
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isEditable: false,
|
||||
@@ -93,9 +93,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
|
||||
isDeclinedByMe: false,
|
||||
isDeclined: false,
|
||||
isVoiceCall: true))
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isEditable: false,
|
||||
@@ -104,9 +104,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
|
||||
isDeclinedByMe: true,
|
||||
isDeclined: true,
|
||||
isVoiceCall: false))
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isEditable: false,
|
||||
@@ -115,9 +115,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
|
||||
isDeclinedByMe: false,
|
||||
isDeclined: true,
|
||||
isVoiceCall: true))
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isEditable: false,
|
||||
@@ -126,9 +126,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
|
||||
isDeclinedByMe: true,
|
||||
isDeclined: true,
|
||||
isVoiceCall: true))
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isEditable: false,
|
||||
@@ -137,9 +137,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
|
||||
isDeclinedByMe: false,
|
||||
isDeclined: true,
|
||||
isVoiceCall: false))
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isEditable: false,
|
||||
@@ -148,7 +148,7 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
|
||||
isDeclinedByMe: true,
|
||||
isDeclined: true,
|
||||
isVoiceCall: false))
|
||||
|
||||
|
||||
Divider()
|
||||
}
|
||||
.padding()
|
||||
|
||||
@@ -73,7 +73,7 @@ struct FormattedBodyText: View {
|
||||
type: .plainText))
|
||||
}
|
||||
let lastPlainTextIndex = components.lastIndex { $0.type == .plainText }
|
||||
|
||||
|
||||
return TimelineBubbleLayout(spacing: 8) {
|
||||
// The `ForEach` needs to iterate over the id of the element to allow
|
||||
// SwiftUI animations to work properly ater any edit.
|
||||
@@ -98,7 +98,7 @@ struct FormattedBodyText: View {
|
||||
.timelineBubbleLayoutSize(.natural)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Make a second iteration through the components adding naturally sized versions of the
|
||||
// block quotes and code blocks which are used for layout calculations but won't be rendered.
|
||||
ForEach(components) { component in
|
||||
|
||||
+2
-2
@@ -91,7 +91,7 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider {
|
||||
static let composerViewModel = ComposerToolbarViewModel.mock()
|
||||
static let timelineViewModel = {
|
||||
let appSettings = AppSettings.volatile()
|
||||
|
||||
|
||||
return TimelineViewModel(roomProxy: roomProxyMock,
|
||||
focussedEventID: focussedEventID,
|
||||
timelineController: MockTimelineController(),
|
||||
@@ -105,7 +105,7 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider {
|
||||
linkMetadataProvider: LinkMetadataProvider(),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
}()
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
ElementNavigationStack {
|
||||
RoomScreen(context: roomViewModel.context,
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ struct ImageRoomTimelineView: View {
|
||||
.timelineMediaFrame(imageInfo: timelineItem.content.thumbnailInfo ?? timelineItem.content.imageInfo)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var placeholder: some View {
|
||||
Rectangle()
|
||||
.foregroundColor(timelineItem.isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming)
|
||||
|
||||
+6
-6
@@ -23,7 +23,7 @@ struct LocationRoomTimelineView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var mainContent: some View {
|
||||
if let geoURI = timelineItem.content.geoURI {
|
||||
@@ -41,16 +41,16 @@ struct LocationRoomTimelineView: View {
|
||||
trailingReservedSize: timelineItem.trailingReservedSize)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
private let mapAspectRatio: Double = 3 / 2
|
||||
private let mapMaxHeight: Double = 300
|
||||
}
|
||||
|
||||
struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = TimelineViewModel.mock
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
PreviewScrollView {
|
||||
VStack(spacing: 8) {
|
||||
@@ -62,7 +62,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
.previewLayout(.sizeThatFits)
|
||||
.previewDisplayName("Bubbles")
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
static var states: some View {
|
||||
LocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
@@ -72,7 +72,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
canBeRepliedTo: true,
|
||||
sender: .init(id: "Bob"),
|
||||
content: .init(body: "Fallback geo uri description")))
|
||||
|
||||
|
||||
LocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
|
||||
@@ -54,24 +54,24 @@ struct PollRoomTimelineView: View {
|
||||
struct PollRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = TimelineViewModel.mock
|
||||
static let pinnedEventsTimelineViewModel = TimelineViewModel.mock(timelineKind: .pinned)
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(), isOutgoing: false))
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Disclosed, Bubble")
|
||||
|
||||
|
||||
PollRoomTimelineView(timelineItem: .mock(poll: .undisclosed(), isOutgoing: false))
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Undisclosed, Bubble")
|
||||
|
||||
|
||||
PollRoomTimelineView(timelineItem: .mock(poll: .endedDisclosed))
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Ended, Disclosed, Bubble")
|
||||
|
||||
|
||||
PollRoomTimelineView(timelineItem: .mock(poll: .endedUndisclosed))
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Ended, Undisclosed, Bubble")
|
||||
|
||||
|
||||
PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(createdByAccountOwner: true)))
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Creator, disclosed, Bubble")
|
||||
|
||||
+4
-4
@@ -29,9 +29,9 @@ struct ReadMarkerRoomTimelineView: View {
|
||||
|
||||
struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = TimelineViewModel.mock
|
||||
|
||||
|
||||
static let item = ReadMarkerRoomTimelineItem(id: .randomVirtual)
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: .init("Separator")), timestamp: .mock)), groupStyle: .single))
|
||||
@@ -42,9 +42,9 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
canBeRepliedTo: true,
|
||||
sender: .init(id: "1", displayName: "Bob"),
|
||||
content: .init(body: "This is another message"))), groupStyle: .single))
|
||||
|
||||
|
||||
ReadMarkerRoomTimelineView(timelineItem: item)
|
||||
|
||||
|
||||
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: .init("Separator")), timestamp: .mock)), groupStyle: .single))
|
||||
RoomTimelineItemView(viewState: .init(type: .text(.init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
|
||||
+1
-1
@@ -9,6 +9,6 @@
|
||||
// periphery: ignore - markdown protocol
|
||||
protocol TextBasedRoomTimelineViewProtocol {
|
||||
associatedtype TimelineItemType: TextBasedRoomTimelineItem
|
||||
|
||||
|
||||
var timelineItem: TimelineItemType { get }
|
||||
}
|
||||
|
||||
+2
-2
@@ -37,11 +37,11 @@ struct VoiceMessageRoomTimelineContent: View {
|
||||
onPlaybackSpeedChange: onPlaybackSpeedChange)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
|
||||
private func onPlaybackPlayPause() {
|
||||
context?.send(viewAction: .handleAudioPlayerAction(.playPause(itemID: timelineItem.id)))
|
||||
}
|
||||
|
||||
|
||||
private func onPlaybackSpeedChange() {
|
||||
context?.send(viewAction: .handleAudioPlayerAction(.changePlaybackSpeed(itemID: timelineItem.id)))
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ import SwiftUI
|
||||
|
||||
struct TimelineSenderAvatarView: View {
|
||||
@Environment(\.timelineContext) private var context
|
||||
|
||||
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
|
||||
|
||||
var body: some View {
|
||||
LoadableAvatarImage(url: timelineItem.sender.avatarURL,
|
||||
name: timelineItem.sender.displayName,
|
||||
|
||||
@@ -79,7 +79,7 @@ struct TimelineView: View {
|
||||
/// A table view wrapper that displays the timeline of a room.
|
||||
struct TimelineViewRepresentable: UIViewControllerRepresentable {
|
||||
@EnvironmentObject private var viewModelContext: TimelineViewModel.Context
|
||||
|
||||
|
||||
func makeUIViewController(context: Context) -> TimelineTableViewController {
|
||||
TimelineTableViewController(coordinator: context.coordinator,
|
||||
isScrolledToBottom: $viewModelContext.isScrolledToBottom,
|
||||
@@ -133,7 +133,7 @@ struct TimelineViewRepresentable: UIViewControllerRepresentable {
|
||||
if tableViewController.readMarkerUniqueID != context.viewState.timelineState.readMarkerUniqueID {
|
||||
tableViewController.readMarkerUniqueID = context.viewState.timelineState.readMarkerUniqueID
|
||||
}
|
||||
|
||||
|
||||
if tableViewController.typingMembers.members != context.viewState.typingMembers {
|
||||
tableViewController.setTypingMembers(context.viewState.typingMembers)
|
||||
}
|
||||
@@ -154,7 +154,7 @@ struct TimelineView_Previews: PreviewProvider { // Not testable as this preview
|
||||
static let composerViewModel = ComposerToolbarViewModel.mock()
|
||||
static let timelineViewModel = {
|
||||
let appSettings = AppSettings.volatile()
|
||||
|
||||
|
||||
return TimelineViewModel(roomProxy: roomProxyMock,
|
||||
timelineController: MockTimelineController(),
|
||||
userSession: UserSessionMock(.init()),
|
||||
@@ -167,7 +167,7 @@ struct TimelineView_Previews: PreviewProvider { // Not testable as this preview
|
||||
linkMetadataProvider: LinkMetadataProvider(),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
}()
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
ElementNavigationStack {
|
||||
RoomScreen(context: roomViewModel.context,
|
||||
|
||||
@@ -15,12 +15,12 @@ struct VoiceMessageRoomPlaybackView: View {
|
||||
@ScaledMetric private var waveformLineWidth = 2.0
|
||||
@ScaledMetric private var waveformLinePadding = 2.0
|
||||
@GestureState var isDragging = false
|
||||
|
||||
|
||||
let onPlayPause: () -> Void
|
||||
let onSeek: (Double) -> Void
|
||||
let onScrubbing: (Bool) -> Void
|
||||
let onPlaybackSpeedChange: () -> Void
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
VoiceMessageButton(state: .init(playerState.playerButtonPlaybackState),
|
||||
@@ -36,7 +36,7 @@ struct VoiceMessageRoomPlaybackView: View {
|
||||
.monospacedDigit()
|
||||
.fixedSize(horizontal: true, vertical: true)
|
||||
}
|
||||
|
||||
|
||||
waveformView
|
||||
.waveformInteraction(isDragging: $isDragging,
|
||||
progress: playerState.progress,
|
||||
@@ -62,7 +62,7 @@ struct VoiceMessageRoomPlaybackView: View {
|
||||
DateFormatter.elapsedTimeFormatter.string(from: duration)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var timeLabelContent: String {
|
||||
// Display the duration if progress is 0.0
|
||||
let percent = playerState.progress > 0.0 ? playerState.progress : 1.0
|
||||
@@ -82,7 +82,7 @@ struct VoiceMessageRoomPlaybackView: View {
|
||||
L10n.a11yVoiceMessage(durationString)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var waveformView: some View {
|
||||
if let url = playerState.fileURL {
|
||||
@@ -96,7 +96,7 @@ struct VoiceMessageRoomPlaybackView: View {
|
||||
estimatedWaveformView
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var estimatedWaveformView: some View {
|
||||
EstimatedWaveformView(lineWidth: waveformLineWidth,
|
||||
linePadding: waveformLinePadding,
|
||||
@@ -111,7 +111,7 @@ private extension DateFormatter {
|
||||
dateFormatter.dateFormat = "m:ss"
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
|
||||
static let longElapsedTimeFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "mm:ss"
|
||||
|
||||
Reference in New Issue
Block a user