mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:31:27 +00:00
Run SwiftFormat.
This commit is contained in:
+2
-2
@@ -126,11 +126,11 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
|
||||
&& selectedRange.location <= upperBound
|
||||
&& selectedRange.length <= upperBound - lowerBound
|
||||
}
|
||||
|
||||
|
||||
guard let match else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
var suggestionText = String(text[match.range])
|
||||
let firstChar = suggestionText.removeFirst()
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ enum ComposerToolbarViewModelAction {
|
||||
case sendMessage(plain: String, html: String?, mode: ComposerMode, intentionalMentions: IntentionalMentions)
|
||||
case editLastMessage
|
||||
case attach(ComposerAttachmentType)
|
||||
|
||||
|
||||
case handlePasteOrDrop(providers: [NSItemProvider])
|
||||
|
||||
|
||||
case composerModeChanged(mode: ComposerMode)
|
||||
case composerFocusedChanged(isFocused: Bool)
|
||||
|
||||
@@ -84,7 +84,7 @@ struct ComposerToolbarViewState: BindableState {
|
||||
var keyCommands: [WysiwygKeyCommand] = []
|
||||
|
||||
var bindings: ComposerToolbarViewStateBindings
|
||||
|
||||
|
||||
var isUploading: Bool {
|
||||
switch composerMode {
|
||||
case .previewVoiceMessage(_, _, let isUploading):
|
||||
@@ -93,7 +93,7 @@ struct ComposerToolbarViewState: BindableState {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var showSendButton: Bool {
|
||||
switch composerMode {
|
||||
case .recordVoiceMessage:
|
||||
@@ -221,7 +221,7 @@ extension FormatItem {
|
||||
return \.link
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var accessibilityIdentifier: String {
|
||||
switch type {
|
||||
case .bold:
|
||||
@@ -250,7 +250,7 @@ extension FormatItem {
|
||||
return A11yIdentifiers.roomScreen.composerToolbar.link
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var accessibilityLabel: String {
|
||||
let localizedAction = switch type {
|
||||
case .bold:
|
||||
@@ -336,7 +336,7 @@ enum ComposerMode: Equatable {
|
||||
case edit(originalEventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, type: EditType)
|
||||
case recordVoiceMessage(state: AudioRecorderState)
|
||||
case previewVoiceMessage(state: AudioPlayerState, waveform: WaveformSource, isUploading: Bool)
|
||||
|
||||
|
||||
var isEdit: Bool {
|
||||
switch self {
|
||||
case .edit:
|
||||
@@ -345,7 +345,7 @@ enum ComposerMode: Equatable {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var isTextEditingEnabled: Bool {
|
||||
switch self {
|
||||
case .default, .reply, .edit:
|
||||
|
||||
+32
-32
@@ -28,23 +28,23 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
private let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||
|
||||
private var hasAppeard = false
|
||||
|
||||
|
||||
private let actionsSubject: PassthroughSubject<ComposerToolbarViewModelAction, Never> = .init()
|
||||
var actions: AnyPublisher<ComposerToolbarViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
private struct WysiwygLinkData {
|
||||
let action: LinkAction
|
||||
let range: NSRange
|
||||
var url: String
|
||||
var text: String
|
||||
}
|
||||
|
||||
|
||||
private var currentLinkData: WysiwygLinkData?
|
||||
|
||||
private var replyLoadingTask: Task<Void, Never>?
|
||||
|
||||
|
||||
init(initialText: String? = nil,
|
||||
roomProxy: JoinedRoomProxyProtocol,
|
||||
wysiwygViewModel: WysiwygComposerViewModel,
|
||||
@@ -84,7 +84,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
.removeDuplicates()
|
||||
.weakAssign(to: \.state.isRoomEncrypted, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
context.$viewState
|
||||
.map(\.composerMode)
|
||||
.removeDuplicates()
|
||||
@@ -93,13 +93,13 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
self?.actionsSubject.send(.composerModeChanged(mode: $0))
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
context.$viewState
|
||||
.map(\.bindings.composerFocused)
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] in self?.actionsSubject.send(.composerFocusedChanged(isFocused: $0)) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
wysiwygViewModel.$isContentEmpty
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] isEmpty in
|
||||
@@ -122,7 +122,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
self?.actionsSubject.send(.contentChanged(isEmpty: plainComposerText.string.isEmpty))
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
wysiwygViewModel.$actionStates
|
||||
.map { actions in
|
||||
FormatType
|
||||
@@ -149,13 +149,13 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
focusComposerIfHardwareKeyboardConnected()
|
||||
|
||||
let identityStatusChangesPublisher = roomProxy.identityStatusChangesPublisher.receive(on: DispatchQueue.main)
|
||||
|
||||
|
||||
Task { [weak self] in
|
||||
for await changes in identityStatusChangesPublisher.values {
|
||||
guard !Task.isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
await self?.processIdentityStatusChanges(changes)
|
||||
}
|
||||
}
|
||||
@@ -176,7 +176,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
func stop() {
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
|
||||
override func process(viewAction: ComposerToolbarViewAction) {
|
||||
switch viewAction {
|
||||
case .composerAppeared:
|
||||
@@ -252,7 +252,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func process(timelineAction: TimelineComposerAction) {
|
||||
switch timelineAction {
|
||||
case .setMode(mode: let mode):
|
||||
@@ -398,7 +398,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
|
||||
private var plainComposerContent: PlainComposerContent {
|
||||
let attributedString = NSMutableAttributedString(attributedString: context.plainComposerText)
|
||||
|
||||
|
||||
var shouldMakeAnotherPass = false
|
||||
var userIDs = Set<String>()
|
||||
var containsAtRoom = false
|
||||
@@ -558,24 +558,24 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
MXLog.error("Failed retrieving room member for identity status change: \(change)")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
identityPinningViolations[change.userId] = member
|
||||
default:
|
||||
// clear
|
||||
identityPinningViolations[change.userId] = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
state.canSend = identityPinningViolations.isEmpty
|
||||
}
|
||||
|
||||
|
||||
private func set(mode: ComposerMode) {
|
||||
if state.composerMode.isLoadingReply, state.composerMode.replyEventID != mode.replyEventID {
|
||||
replyLoadingTask?.cancel()
|
||||
}
|
||||
|
||||
guard mode != state.composerMode else { return }
|
||||
|
||||
|
||||
state.composerMode = mode
|
||||
switch mode {
|
||||
case .default:
|
||||
@@ -589,14 +589,14 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
state.bindings.composerFocused = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func set(text: String) {
|
||||
if context.composerFormattingEnabled {
|
||||
wysiwygViewModel.textView.flushPills()
|
||||
wysiwygViewModel.setHtmlContent(text)
|
||||
} else {
|
||||
let attributedString = NSMutableAttributedString(string: text)
|
||||
|
||||
|
||||
parseUserMentionsMarkdown(text) { range, url in
|
||||
// Call your handleUserMention function here
|
||||
attributedString.addAttribute(.link, value: url, range: range)
|
||||
@@ -649,19 +649,19 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
range: wysiwygViewModel.attributedContent.selection,
|
||||
url: linkAction.url ?? "",
|
||||
text: "")
|
||||
|
||||
|
||||
let urlBinding: Binding<String> = .init { [weak self] in
|
||||
self?.currentLinkData?.url ?? ""
|
||||
} set: { [weak self] value in
|
||||
self?.currentLinkData?.url = value
|
||||
}
|
||||
|
||||
|
||||
let textBinding: Binding<String> = .init { [weak self] in
|
||||
self?.currentLinkData?.text ?? ""
|
||||
} set: { [weak self] value in
|
||||
self?.currentLinkData?.text = value
|
||||
}
|
||||
|
||||
|
||||
switch linkAction {
|
||||
case .createWithText:
|
||||
state.bindings.alertInfo = makeCreateWithTextAlertInfo(urlBinding: urlBinding, textBinding: textBinding)
|
||||
@@ -673,7 +673,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func makeCreateWithTextAlertInfo(urlBinding: Binding<String>, textBinding: Binding<String>) -> AlertInfo<UUID> {
|
||||
AlertInfo(id: UUID(),
|
||||
title: L10n.richTextEditorCreateLink,
|
||||
@@ -683,7 +683,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
secondaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionSave) {
|
||||
self.restoreComposerSelectedRange()
|
||||
self.createLinkWithText()
|
||||
|
||||
|
||||
},
|
||||
textFields: [AlertInfo<UUID>.AlertTextField(placeholder: L10n.commonText,
|
||||
text: textBinding,
|
||||
@@ -694,7 +694,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
autoCapitalization: .never,
|
||||
autoCorrectionDisabled: true)])
|
||||
}
|
||||
|
||||
|
||||
private func makeSetUrlAlertInfo(urlBinding: Binding<String>, isEdit: Bool) -> AlertInfo<UUID> {
|
||||
AlertInfo(id: UUID(),
|
||||
title: isEdit ? L10n.richTextEditorEditLink : L10n.richTextEditorCreateLink,
|
||||
@@ -704,14 +704,14 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
secondaryButton: AlertInfo<UUID>.AlertButton(title: L10n.actionSave) {
|
||||
self.restoreComposerSelectedRange()
|
||||
self.setLink()
|
||||
|
||||
|
||||
},
|
||||
textFields: [AlertInfo<UUID>.AlertTextField(placeholder: L10n.richTextEditorUrlPlaceholder,
|
||||
text: urlBinding,
|
||||
autoCapitalization: .never,
|
||||
autoCorrectionDisabled: true)])
|
||||
}
|
||||
|
||||
|
||||
private func makeEditChoiceAlertInfo(urlBinding: Binding<String>) -> AlertInfo<UUID> {
|
||||
AlertInfo(id: UUID(),
|
||||
title: L10n.richTextEditorEditLink,
|
||||
@@ -726,23 +726,23 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
}
|
||||
}])
|
||||
}
|
||||
|
||||
|
||||
private func restoreComposerSelectedRange() {
|
||||
guard let currentLinkData else { return }
|
||||
wysiwygViewModel.select(range: currentLinkData.range)
|
||||
}
|
||||
|
||||
|
||||
private func setLink() {
|
||||
guard let currentLinkData else { return }
|
||||
wysiwygViewModel.applyLinkOperation(.setLink(urlString: currentLinkData.url))
|
||||
}
|
||||
|
||||
|
||||
private func createLinkWithText() {
|
||||
guard let currentLinkData else { return }
|
||||
wysiwygViewModel.applyLinkOperation(.createLink(urlString: currentLinkData.url,
|
||||
text: currentLinkData.text))
|
||||
}
|
||||
|
||||
|
||||
private func removeLinks() {
|
||||
wysiwygViewModel.applyLinkOperation(.removeLinks)
|
||||
}
|
||||
@@ -819,7 +819,7 @@ extension ComposerToolbarViewModel {
|
||||
if !canSend {
|
||||
roomProxy.identityStatusChangesPublisher = .init([.init(userId: RoomMemberProxyMock.mockAlice.userID, changedTo: .verificationViolation)])
|
||||
}
|
||||
|
||||
|
||||
let wysiwygViewModel = WysiwygComposerViewModel()
|
||||
let viewModel = ComposerToolbarViewModel(roomProxy: roomProxy,
|
||||
wysiwygViewModel: wysiwygViewModel,
|
||||
|
||||
+1
-1
@@ -17,6 +17,6 @@ protocol ComposerToolbarViewModelProtocol {
|
||||
|
||||
func start()
|
||||
func stop()
|
||||
|
||||
|
||||
func process(timelineAction: TimelineComposerAction)
|
||||
}
|
||||
|
||||
+6
-6
@@ -23,7 +23,7 @@ struct CompletionSuggestionView: View {
|
||||
/// To make the scrolling more apparent we show a factional amount
|
||||
static let maxVisibleRows: CGFloat = 4.5
|
||||
}
|
||||
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@State private var prototypeListItemFrame: CGRect = .zero
|
||||
@@ -47,7 +47,7 @@ struct CompletionSuggestionView: View {
|
||||
.padding(.bottom, Constants.listItemPadding)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func list() -> some View {
|
||||
List(items) { item in
|
||||
Button {
|
||||
@@ -66,18 +66,18 @@ struct CompletionSuggestionView: View {
|
||||
private func contentHeightForRowCount(_ count: CGFloat) -> CGFloat {
|
||||
(prototypeListItemFrame.height + Constants.listItemPadding * 2 + Constants.listItemSpacing) * count - Constants.listItemSpacing / 2 + Constants.topPadding - Constants.listItemPadding
|
||||
}
|
||||
|
||||
|
||||
private struct ListItemPaddingModifier: ViewModifier {
|
||||
private let isFirst: Bool
|
||||
|
||||
|
||||
init(isFirst: Bool) {
|
||||
self.isFirst = isFirst
|
||||
}
|
||||
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
let topPadding: CGFloat = isFirst ? Constants.topPadding : Constants.listItemPadding
|
||||
let bottomPadding: CGFloat = Constants.listItemPadding
|
||||
|
||||
|
||||
return content
|
||||
.padding(.top, topPadding)
|
||||
.padding(.bottom, bottomPadding)
|
||||
|
||||
@@ -236,7 +236,7 @@ struct ComposerToolbar: View {
|
||||
// sending e.g. accepting current autocorrection.
|
||||
// Fixes https://github.com/element-hq/element-x-ios/issues/3216
|
||||
context.presendCallback?()
|
||||
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
context.send(viewAction: .sendMessage)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ struct FormattingToolbar: View {
|
||||
var formatItems: [FormatItem]
|
||||
/// The action when an item is selected
|
||||
var formatAction: (FormatType) -> Void
|
||||
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal) {
|
||||
HStack(spacing: 5) {
|
||||
@@ -50,7 +50,7 @@ private extension FormatItem {
|
||||
return .compound.iconDisabled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var backgroundColor: Color {
|
||||
switch state {
|
||||
case .reversed:
|
||||
|
||||
@@ -78,7 +78,7 @@ struct MessageComposer: View {
|
||||
pasteHandler: pasteAction)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var composerHeight: CGFloat {
|
||||
let baseHeight = isExpanded ? ComposerConstant.maxHeight : ComposerConstant.minHeight
|
||||
return (baseHeight - composerTranslation).clamped(to: ComposerConstant.allowedHeightRange)
|
||||
@@ -95,7 +95,7 @@ struct MessageComposer: View {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var resizeGrabber: some View {
|
||||
Capsule()
|
||||
.foregroundStyle(.tertiary)
|
||||
@@ -103,7 +103,7 @@ struct MessageComposer: View {
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
|
||||
private var dragGesture: some Gesture {
|
||||
DragGesture()
|
||||
.onChanged { value in
|
||||
|
||||
+30
-30
@@ -14,11 +14,11 @@ struct MessageComposerTextField: View {
|
||||
@Binding var text: NSAttributedString
|
||||
@Binding var presendCallback: (() -> Void)?
|
||||
@Binding var selectedRange: NSRange
|
||||
|
||||
|
||||
let maxHeight: CGFloat
|
||||
let keyHandler: GenericKeyHandler
|
||||
let pasteHandler: PasteHandler
|
||||
|
||||
|
||||
var body: some View {
|
||||
UITextViewWrapper(text: $text,
|
||||
presendCallback: $presendCallback,
|
||||
@@ -30,7 +30,7 @@ struct MessageComposerTextField: View {
|
||||
.background(placeholderView, alignment: .topLeading)
|
||||
.background { keyboardShortcuts }
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var placeholderView: some View {
|
||||
if text.string.isEmpty {
|
||||
@@ -54,18 +54,18 @@ struct MessageComposerTextField: View {
|
||||
|
||||
private struct UITextViewWrapper: UIViewRepresentable {
|
||||
@Environment(\.timelineContext) private var timelineContext
|
||||
|
||||
|
||||
@Binding var text: NSAttributedString
|
||||
@Binding var presendCallback: (() -> Void)?
|
||||
@Binding var selectedRange: NSRange
|
||||
|
||||
|
||||
let maxHeight: CGFloat
|
||||
|
||||
|
||||
let keyHandler: GenericKeyHandler
|
||||
let pasteHandler: PasteHandler
|
||||
|
||||
|
||||
private let font = UIFont.preferredFont(forTextStyle: .body)
|
||||
|
||||
|
||||
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
|
||||
// Need to use TextKit 1 for mentions
|
||||
let textView = ElementTextView(timelineContext: timelineContext,
|
||||
@@ -89,21 +89,21 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
textView.autocorrectionType = .no
|
||||
}
|
||||
|
||||
|
||||
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
|
||||
return textView
|
||||
}
|
||||
|
||||
|
||||
func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextView, context: Context) -> CGSize? {
|
||||
// Note: Coalescing a width of zero here returns a size for the view with 1 line of text visible.
|
||||
let newSize = uiView.sizeThatFits(CGSize(width: proposal.width ?? .zero, height: maxHeight))
|
||||
let width = proposal.width ?? newSize.width
|
||||
let height = min(maxHeight, newSize.height)
|
||||
|
||||
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
|
||||
|
||||
func updateUIView(_ textView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
|
||||
// Prevent the textView from inheriting attributes from mention pills
|
||||
textView.typingAttributes = [.font: font,
|
||||
@@ -149,7 +149,7 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(text: $text,
|
||||
selectedRange: $selectedRange,
|
||||
@@ -157,16 +157,16 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
keyHandler: keyHandler,
|
||||
pasteHandler: pasteHandler)
|
||||
}
|
||||
|
||||
|
||||
final class Coordinator: NSObject, UITextViewDelegate, ElementTextViewDelegate {
|
||||
private var text: Binding<NSAttributedString>
|
||||
private var selectedRange: Binding<NSRange>
|
||||
|
||||
|
||||
private let maxHeight: CGFloat
|
||||
|
||||
|
||||
private let keyHandler: GenericKeyHandler
|
||||
private let pasteHandler: PasteHandler
|
||||
|
||||
|
||||
init(text: Binding<NSAttributedString>,
|
||||
selectedRange: Binding<NSRange>,
|
||||
maxHeight: CGFloat,
|
||||
@@ -178,11 +178,11 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
self.keyHandler = keyHandler
|
||||
self.pasteHandler = pasteHandler
|
||||
}
|
||||
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
text.wrappedValue = textView.attributedText
|
||||
}
|
||||
|
||||
|
||||
func textViewDidReceiveKeyPress(_ textView: UITextView, key: UIKeyboardHIDUsage) {
|
||||
keyHandler(key)
|
||||
}
|
||||
@@ -190,7 +190,7 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
func textViewDidReceiveShiftEnterKeyPress(_ textView: UITextView) {
|
||||
textView.insertText("\n")
|
||||
}
|
||||
|
||||
|
||||
func textView(_ textView: UITextView, didReceivePasteWith providers: [NSItemProvider]) {
|
||||
pasteHandler(providers)
|
||||
}
|
||||
@@ -242,7 +242,7 @@ private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate {
|
||||
[UIKeyCommand(input: "\r", modifierFlags: .shift, action: #selector(shiftEnterKeyPressed)),
|
||||
UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(enterKeyPressed))]
|
||||
}
|
||||
|
||||
|
||||
@objc func shiftEnterKeyPressed(sender: UIKeyCommand) {
|
||||
elementDelegate?.textViewDidReceiveShiftEnterKeyPress(self)
|
||||
}
|
||||
@@ -269,21 +269,21 @@ private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate {
|
||||
|
||||
super.pressesBegan(presses, with: event)
|
||||
}
|
||||
|
||||
|
||||
// Pasting support
|
||||
|
||||
|
||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
if super.canPerformAction(action, withSender: sender) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
guard action == #selector(paste(_:)) else {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
return UIPasteboard.general.itemProviders.filter { !$0.isSupportedForPasteOrDrop }.isEmpty
|
||||
}
|
||||
|
||||
|
||||
override func paste(_ sender: Any?) {
|
||||
let providers = UIPasteboard.general.itemProviders
|
||||
|
||||
@@ -312,7 +312,7 @@ private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate {
|
||||
func registerPillView(_ pillView: UIView) {
|
||||
pillViews.add(pillView)
|
||||
}
|
||||
|
||||
|
||||
func flushPills() {
|
||||
for view in pillViews.allObjects {
|
||||
view.alpha = 0.0
|
||||
@@ -341,15 +341,15 @@ struct MessageComposerTextField_Previews: PreviewProvider, TestablePreview {
|
||||
PreviewWrapper(text: "A really long message that will wrap to multiple lines on a phone in portrait.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct PreviewWrapper: View {
|
||||
@State var text: NSAttributedString
|
||||
|
||||
|
||||
init(text: String) {
|
||||
_text = .init(initialValue: .init(string: text, attributes: [.font: UIFont.preferredFont(forTextStyle: .body),
|
||||
.foregroundColor: UIColor.compound.textPrimary]))
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
MessageComposerTextField(placeholder: "Placeholder",
|
||||
text: $text,
|
||||
|
||||
@@ -79,10 +79,10 @@ struct RoomAttachmentPicker: View {
|
||||
|
||||
struct RoomAttachmentPicker_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = makeViewModel()
|
||||
|
||||
|
||||
static func makeViewModel() -> ComposerToolbarViewModel {
|
||||
let appSettings = AppSettings.volatile()
|
||||
|
||||
|
||||
return ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()),
|
||||
wysiwygViewModel: WysiwygComposerViewModel(),
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
@@ -92,7 +92,7 @@ struct RoomAttachmentPicker_Previews: PreviewProvider, TestablePreview {
|
||||
analyticsService: AnalyticsServiceMock(.init()),
|
||||
composerDraftService: ComposerDraftServiceMock(.init()))
|
||||
}
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
RoomAttachmentPicker(context: viewModel.context)
|
||||
}
|
||||
|
||||
+4
-4
@@ -18,12 +18,12 @@ struct VoiceMessagePreviewComposer: View {
|
||||
@ScaledMetric private var waveformLineWidth = 2.0
|
||||
@ScaledMetric private var waveformLinePadding = 2.0
|
||||
@GestureState var isDragging = false
|
||||
|
||||
|
||||
let onPlay: () -> Void
|
||||
let onPause: () -> Void
|
||||
let onSeek: (Double) -> Void
|
||||
let onScrubbing: (Bool) -> Void
|
||||
|
||||
|
||||
var timeLabelContent: String {
|
||||
// Display the duration if progress is 0.0
|
||||
let percent = playerState.progress > 0.0 ? playerState.progress : 1.0
|
||||
@@ -43,7 +43,7 @@ struct VoiceMessagePreviewComposer: View {
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.monospacedDigit()
|
||||
.fixedSize(horizontal: true, vertical: true)
|
||||
|
||||
|
||||
waveformView
|
||||
.waveformInteraction(isDragging: $isDragging,
|
||||
progress: playerState.progress,
|
||||
@@ -77,7 +77,7 @@ struct VoiceMessagePreviewComposer: View {
|
||||
.progressMask(progress: playerState.progress)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func onPlayPause() {
|
||||
if playerState.playbackState == .playing {
|
||||
onPause()
|
||||
|
||||
+4
-4
@@ -17,13 +17,13 @@ struct VoiceMessageRecordingView: View {
|
||||
@ScaledMetric private var waveformLineWidth = 2.0
|
||||
@ScaledMetric private var waveformLinePadding = 2.0
|
||||
@ScaledMetric private var recordingIndicatorSize = 8
|
||||
|
||||
|
||||
private static let elapsedTimeFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "mm:ss"
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
|
||||
private var timeLabelContent: String {
|
||||
Self.elapsedTimeFormatter.string(from: Date(timeIntervalSinceReferenceDate: recorderState.duration))
|
||||
}
|
||||
@@ -37,7 +37,7 @@ struct VoiceMessageRecordingView: View {
|
||||
HStack(spacing: 8) {
|
||||
VoiceMessageRecordingBadge()
|
||||
.frame(width: recordingIndicatorSize, height: recordingIndicatorSize)
|
||||
|
||||
|
||||
Text(timeLabelContent)
|
||||
.lineLimit(1)
|
||||
.font(.compound.bodySMSemibold)
|
||||
@@ -55,7 +55,7 @@ struct VoiceMessageRecordingView: View {
|
||||
|
||||
private struct VoiceMessageRecordingBadge: View {
|
||||
@State private var opacity: CGFloat = 0
|
||||
|
||||
|
||||
var body: some View {
|
||||
Circle()
|
||||
.foregroundColor(.red)
|
||||
|
||||
@@ -57,9 +57,9 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
private var roomViewModel: RoomScreenViewModelProtocol
|
||||
private var timelineViewModel: TimelineViewModelProtocol
|
||||
private var composerViewModel: ComposerToolbarViewModelProtocol
|
||||
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
|
||||
private let actionsSubject: PassthroughSubject<RoomScreenCoordinatorAction, Never> = .init()
|
||||
var actions: AnyPublisher<RoomScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
@@ -115,7 +115,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
timelineViewModel.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
|
||||
switch action {
|
||||
case .displayEmojiPicker(let itemID, let selectedEmojis):
|
||||
actionsSubject.send(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis))
|
||||
@@ -167,11 +167,11 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
composerViewModel.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
|
||||
timelineViewModel.process(composerAction: action)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -240,7 +240,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
let composerToolbar = ComposerToolbar(context: composerViewModel.context)
|
||||
|
||||
|
||||
return AnyView(RoomScreen(context: roomViewModel.context,
|
||||
timelineContext: timelineViewModel.context,
|
||||
composerToolbar: composerToolbar))
|
||||
|
||||
@@ -66,7 +66,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
|
||||
self.initialSelectedPinnedEventID = initialSelectedPinnedEventID
|
||||
pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID)
|
||||
|
||||
|
||||
let viewState = RoomScreenViewState(roomTitle: roomProxy.infoPublisher.value.displayName ?? roomProxy.id,
|
||||
roomAvatar: roomProxy.infoPublisher.value.avatar,
|
||||
hasOngoingCall: roomProxy.infoPublisher.value.hasRoomCall,
|
||||
@@ -83,7 +83,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
await updateVerificationBadge()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func process(viewAction: RoomScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .tappedPinnedEventsBanner:
|
||||
@@ -174,7 +174,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
state.isSharingLiveLocation = sessionsByRoomID.keys.contains(roomProxy.id)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
roomProxy.infoPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] roomInfo in
|
||||
@@ -251,7 +251,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
MXLog.error("Failed retrieving room member for identity status change: \(change)")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
identityVerificationViolations[change.userId] = member
|
||||
default:
|
||||
identityVerificationViolations[change.userId] = nil
|
||||
@@ -303,9 +303,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
defer {
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
|
||||
|
||||
showLoadingIndicator()
|
||||
|
||||
|
||||
if case .failure = await clientProxy.withdrawUserIdentityVerification(userID) {
|
||||
state.bindings.alertInfo = .init(id: .unknown, title: L10n.commonError)
|
||||
}
|
||||
@@ -352,7 +352,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
default:
|
||||
state.isKnockableRoom = false
|
||||
}
|
||||
|
||||
|
||||
if let powerLevels = roomInfo.powerLevels {
|
||||
state.canSendMessage = powerLevels.canOwnUser(sendMessage: .roomMessage)
|
||||
state.canJoinCall = powerLevels.canOwnUserJoinCall()
|
||||
@@ -379,7 +379,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func acceptKnock(eventID: String) async {
|
||||
guard case let .loaded(requests) = roomProxy.knockRequestsStatePublisher.value,
|
||||
let request = requests.first(where: { $0.eventID == eventID }) else {
|
||||
|
||||
@@ -33,7 +33,7 @@ struct RoomCallControlsToolbar: ToolbarContent {
|
||||
} label: {
|
||||
Label(L10n.a11yStartVoiceCall, icon: \.voiceCallSolid)
|
||||
}
|
||||
|
||||
|
||||
Button {
|
||||
onCallTap(false)
|
||||
} label: {
|
||||
|
||||
@@ -16,16 +16,16 @@ struct RoomScreen: View {
|
||||
@ObservedObject private var timelineContext: TimelineViewModelType.Context
|
||||
let composerToolbar: ComposerToolbar
|
||||
@Environment(\.accessibilityVoiceOverEnabled) private var isVoiceOverEnabled
|
||||
|
||||
|
||||
enum MarkAsReadSource {
|
||||
case up
|
||||
case down
|
||||
}
|
||||
|
||||
|
||||
/// Which scroll button (if any) currently has the "Mark as read" pill displayed alongside it.
|
||||
/// Set when the user long-presses one of the scroll buttons; the pill anchors to that button.
|
||||
@State private var markAsReadSource: MarkAsReadSource?
|
||||
|
||||
|
||||
init(context: RoomScreenViewModelType.Context,
|
||||
timelineContext: TimelineViewModelType.Context,
|
||||
composerToolbar: ComposerToolbar) {
|
||||
@@ -33,7 +33,7 @@ struct RoomScreen: View {
|
||||
self.timelineContext = timelineContext
|
||||
self.composerToolbar = composerToolbar
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
TimelineView(timelineContext: timelineContext)
|
||||
.overlay {
|
||||
@@ -183,7 +183,7 @@ struct RoomScreen: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var markAsReadPill: some View {
|
||||
Button {
|
||||
timelineContext.send(viewAction: .markAllAsRead)
|
||||
@@ -192,7 +192,7 @@ struct RoomScreen: View {
|
||||
markAsReadPillLabel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var markAsReadPillLabel: some View {
|
||||
// Font scales with Dynamic Type via the Compound token; padding is a fixed point value
|
||||
@@ -212,15 +212,15 @@ struct RoomScreen: View {
|
||||
label.background(.regularMaterial, in: Capsule())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func revealMarkAsReadPill(source: MarkAsReadSource) {
|
||||
markAsReadSource = source
|
||||
}
|
||||
|
||||
|
||||
private func dismissMarkAsReadPill() {
|
||||
markAsReadSource = nil
|
||||
}
|
||||
|
||||
|
||||
/// Whether the scroll button that the pill is anchored to is still being rendered.
|
||||
/// Used to dismiss an orphaned pill when the source button gets hidden — without
|
||||
/// this, the pill can render alongside an invisible button. Returns `true` when no
|
||||
@@ -232,13 +232,13 @@ struct RoomScreen: View {
|
||||
case .none: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Hide the new-messages dot when the jump-to-read-marker feature is disabled.
|
||||
private var scrollToBottomShowsBadge: Bool {
|
||||
timelineContext.viewState.jumpToReadMarkerEnabled
|
||||
&& timelineContext.viewState.bindings.hasNewMessagesAtBottom
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var composer: some View {
|
||||
if context.viewState.hasSuccessor {
|
||||
@@ -328,7 +328,7 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let readOnlyViewModels = makeViewModels(canSendMessage: false)
|
||||
static let tombstonedViewModels = makeViewModels(hasSuccessor: true)
|
||||
static let composerViewModel = ComposerToolbarViewModel.mock()
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
ElementNavigationStack {
|
||||
RoomScreen(context: viewModels.room.context,
|
||||
@@ -361,7 +361,7 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview {
|
||||
successor: hasSuccessor ? .init(roomId: UUID().uuidString, reason: nil) : nil,
|
||||
powerLevelsConfiguration: .init(canUserSendMessage: canSendMessage)))
|
||||
let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock)
|
||||
|
||||
|
||||
let appSettings = AppSettings.volatile()
|
||||
let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock,
|
||||
timelineController: MockTimelineController(),
|
||||
|
||||
@@ -225,7 +225,7 @@ private struct PanGestureRepresentable: UIGestureRecognizerRepresentable {
|
||||
|
||||
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
guard let panRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { return false }
|
||||
|
||||
|
||||
let velocity = panRecognizer.velocity(in: gestureRecognizer.view)
|
||||
return abs(velocity.y) < abs(velocity.x)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user