Skip to content

Commit

Permalink
Edit Private Messages (#1567)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sjmarf authored Jan 3, 2025
1 parent 41f2fbc commit 31e9a22
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 76 deletions.
1 change: 1 addition & 0 deletions Mlem/App/Configuration/Icons.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ enum Icons {
static let replyFill: String = "arrowshape.turn.up.left.fill"
static let send: String = "paperplane"
static let sendFill: String = "paperplane.fill"
static let sendMessage: String = "arrow.up.circle.fill"

// save
static let save: String = "bookmark"
Expand Down
4 changes: 2 additions & 2 deletions Mlem/App/Globals/Definitions/ErrorsTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ class ErrorsTracker {
static var main: ErrorsTracker = .init()

func createErrorLog() -> String {
var ret: String = ""
var ret = ""

errors.forEach { details in
for details in errors {
let description = details.error?.localizedDescription ?? "No Description"
ret += "\(details.when.formatted(.iso8601))\t\(details.title ?? "Error")\t\(description)\n"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ extension Message1Providing {
func allMenuActions(
feedback: Set<FeedbackType> = [.haptic, .toast],
isInMessageFeed: Bool = false,
editCallback: (@MainActor () -> Void)?,
navigation: NavigationLayer? = nil,
report: Report? = nil
) -> [any Action] {
basicMenuActions(
feedback: feedback,
isInMessageFeed: isInMessageFeed,
editCallback: editCallback,
navigation: navigation
)
if api.isAdmin {
Expand All @@ -47,6 +49,7 @@ extension Message1Providing {
func basicMenuActions(
feedback: Set<FeedbackType> = [.haptic, .toast],
isInMessageFeed: Bool = false,
editCallback: (@MainActor () -> Void)?,
navigation: NavigationLayer? = nil,
report: Report? = nil
) -> [any Action] {
Expand All @@ -60,6 +63,9 @@ extension Message1Providing {
selectTextAction()
}
if isOwnMessage {
if let editCallback {
editAction(callback: editCallback)
}
deleteAction(feedback: feedback)
} else {
if report == nil {
Expand All @@ -83,6 +89,14 @@ extension Message1Providing {
}
}

func editAction(callback: @escaping @MainActor () -> Void) -> BasicAction {
.init(
id: "edit\(uid)",
appearance: .edit(),
callback: api.canInteract ? callback : nil
)
}

// These actions are also defined in Interactable1Providing... another protocol for these may be a good idea

func replyAction(navigation: NavigationLayer) -> BasicAction {
Expand Down
3 changes: 2 additions & 1 deletion Mlem/App/Views/Pages/MessageFeedView/MessageBubbleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct MessageBubbleView: View {
@Environment(Palette.self) var palette

let message: any Message
var editCallback: @MainActor () -> Void

var body: some View {
Group {
Expand All @@ -33,7 +34,7 @@ struct MessageBubbleView: View {
)
.contentShape(.contextMenuPreview, BubbleShape(myMessage: message.isOwnMessage))
.contextMenu {
message.allMenuActions(isInMessageFeed: true, navigation: navigation)
message.allMenuActions(isInMessageFeed: true, editCallback: editCallback, navigation: navigation)
}
}
}
Expand Down
37 changes: 37 additions & 0 deletions Mlem/App/Views/Pages/MessageFeedView/MessageFeedView+Logic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,41 @@ extension MessageFeedView {
handleError(error)
}
}

func editMessage(_ message: any Message) async {
do {
try await message.edit(content: textView.text)
editing = nil
textView.text = ""
textView.resignFirstResponder()
} catch {
handleError(error)
}
}

func messageIsFirstOfDay(_ message: Message2) -> Bool {
guard let feedLoader else { return false }
guard let index = feedLoader.items.firstIndex(of: message) else {
assertionFailure()
return false
}
guard index < feedLoader.items.count - 1 else { return true }
let previousMessage = feedLoader.items[index + 1]
return !Calendar.current.isDate(previousMessage.created, inSameDayAs: message.created)
}

var minTextEditorHeight: CGFloat {
Constants.main.standardSpacing * 2 + UIFont.preferredFont(forTextStyle: .body).lineHeight
}

func messageFooterText(for message: Message2) -> String? {
var parts: [String] = .init()
if message == feedLoader?.items.first, Calendar.current.isDateInToday(message.created) {
parts.append(message.created.formatted(date: .omitted, time: .shortened))
}
if message.updated != nil {
parts.append(.init(localized: "Edited"))
}
return parts.joined(separator: "")
}
}
135 changes: 82 additions & 53 deletions Mlem/App/Views/Pages/MessageFeedView/MessageFeedView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@ struct MessageFeedView: View {

let person: AnyPerson
let focusTextField: Bool
@State var editing: (any Message)?

let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()

init(person: AnyPerson, focusTextField: Bool) {
init(person: AnyPerson, focusTextField: Bool, editing: (any Message)?) {
self.person = person
self.focusTextField = focusTextField
self._editing = .init(wrappedValue: editing)
if let editing {
let textView = UITextView()
textView.text = editing.content
_textView = .init(wrappedValue: textView)
}
}

@State var feedLoader: MessageFeedLoader?
Expand All @@ -41,20 +48,7 @@ struct MessageFeedView: View {
CloseButtonView()
}
} else {
ToolbarItem(placement: .principal) {
NavigationLink(.person(person)) {
HStack(spacing: Constants.main.halfSpacing) {
CircleCroppedImageView(person, frame: 24)
Text(person.displayName)
.foregroundStyle(palette.primary)
.font(.headline)
Image(systemName: Icons.forward)
.imageScale(.small)
.fontWeight(.semibold)
.foregroundStyle(palette.tertiary)
}
}
}
ToolbarItem(placement: .principal) { navigationTitleView(person: person) }
ToolbarItemGroup(placement: .secondaryAction) {
SwiftUI.Section {
if person is any Person3Providing, proxy.isLoading {
Expand All @@ -77,8 +71,7 @@ struct MessageFeedView: View {
}
}

@ViewBuilder
func content(person: any Person) -> some View {
@ViewBuilder func content(person: any Person) -> some View {
ScrollViewReader { scrollProxy in
ScrollView {
if let feedLoader {
Expand Down Expand Up @@ -108,9 +101,7 @@ struct MessageFeedView: View {
}
}
}
.safeAreaInset(edge: .bottom) {
textInput(scrollProxy)
}
.safeAreaInset(edge: .bottom) { textInput(scrollProxy) }
.defaultScrollAnchor(.bottom)
.scrollDismissesKeyboard(.interactively)
.background(palette.groupedBackground)
Expand Down Expand Up @@ -138,18 +129,22 @@ struct MessageFeedView: View {
.padding(.bottom, Constants.main.halfSpacing)
}
VStack(alignment: message.isOwnMessage ? .trailing : .leading, spacing: Constants.main.halfSpacing) {
MessageBubbleView(message: message)
.padding(message.isOwnMessage ? .leading : .trailing, 50)
.frame(maxWidth: 400, alignment: message.isOwnMessage ? .trailing : .leading)
.onAppear {
do {
try feedLoader.loadIfThreshold(message)
} catch {
handleError(error)
}
MessageBubbleView(message: message, editCallback: {
editing = message
textView.text = message.content
textView.becomeFirstResponder()
})
.padding(message.isOwnMessage ? .leading : .trailing, 50)
.frame(maxWidth: 400, alignment: message.isOwnMessage ? .trailing : .leading)
.onAppear {
do {
try feedLoader.loadIfThreshold(message)
} catch {
handleError(error)
}
if message === feedLoader.items.first, Calendar.current.isDateInToday(message.created) {
Text(message.created.formatted(date: .omitted, time: .shortened))
}
if let footerText = messageFooterText(for: message) {
Text(footerText)
.font(.footnote)
.foregroundStyle(palette.secondary)
.padding(.horizontal, Constants.main.halfSpacing)
Expand All @@ -158,30 +153,22 @@ struct MessageFeedView: View {
.padding([.horizontal, .bottom], Constants.main.standardSpacing)
}

var minTextEditorHeight: CGFloat {
Constants.main.standardSpacing * 2 + UIFont.preferredFont(forTextStyle: .body).lineHeight
}

@ViewBuilder
func textInput(_ scrollProxy: ScrollViewProxy) -> some View {
OptimalHeightLayout {
HStack(alignment: .bottom) {
ScrollView { textInputView(scrollProxy) }
.scrollBounceBehavior(.basedOnSize, axes: .vertical)
.scrollIndicators(.hidden)
Button {
Task { @MainActor in
await sendMessage(scrollProxy)
HStack(spacing: 6) {
if editing != nil {
cancelEditButton()
}
} label: {
Image(systemName: "arrow.up.circle.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: minTextEditorHeight - 12)
.fontWeight(.semibold)
.foregroundStyle(palette.selectedInteractionBarItem, palette.accent)
sendButton(scrollProxy)
}
.frame(height: minTextEditorHeight - 12)
.padding(6)
.fontWeight(.semibold)
}
.frame(minHeight: minTextEditorHeight, maxHeight: 200)
}
Expand All @@ -193,6 +180,43 @@ struct MessageFeedView: View {
.background(.bar)
}

@ViewBuilder
func cancelEditButton() -> some View {
Button {
editing = nil
textView.text = ""
textView.resignFirstResponder()
} label: {
textInputButtonLabel(systemImage: Icons.closeCircleFill)
}
.tint(palette.tertiary)
}

@ViewBuilder
func sendButton(_ scrollProxy: ScrollViewProxy) -> some View {
Button {
Task { @MainActor in
if let editing {
await editMessage(editing)
} else {
await sendMessage(scrollProxy)
}
}
} label: {
textInputButtonLabel(systemImage: editing == nil ? Icons.sendMessage : Icons.successCircleFill)
}
.tint(palette.accent)
}

@ViewBuilder
func textInputButtonLabel(systemImage: String) -> some View {
Image(systemName: systemImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxHeight: .infinity)
.foregroundStyle(palette.selectedInteractionBarItem, .tint)
}

@ViewBuilder
func textInputView(_ scrollProxy: ScrollViewProxy) -> some View {
MarkdownTextEditor(
Expand Down Expand Up @@ -227,14 +251,19 @@ struct MessageFeedView: View {
)
}

func messageIsFirstOfDay(_ message: Message2) -> Bool {
guard let feedLoader else { return false }
guard let index = feedLoader.items.firstIndex(of: message) else {
assertionFailure()
return false
@ViewBuilder
func navigationTitleView(person: any Person) -> some View {
NavigationLink(.person(person)) {
HStack(spacing: Constants.main.halfSpacing) {
CircleCroppedImageView(person, frame: 24)
Text(person.displayName)
.foregroundStyle(palette.primary)
.font(.headline)
Image(systemName: Icons.forward)
.imageScale(.small)
.fontWeight(.semibold)
.foregroundStyle(palette.tertiary)
}
}
guard index < feedLoader.items.count - 1 else { return true }
let previousMessage = feedLoader.items[index + 1]
return !Calendar.current.isDate(previousMessage.created, inSameDayAs: message.created)
}
}
20 changes: 10 additions & 10 deletions Mlem/App/Views/Root/Tabs/Settings/DeveloperSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
// Created by Sjmarf on 22/09/2024.
//

import SwiftUI
import MlemMiddleware
import SwiftUI

// Strings in this view are intentionally left unlocalized; we shouldn't
// be burdening translators with these when they'll never be used
Expand All @@ -23,17 +23,17 @@ struct DeveloperSettingsView: View {
}

#if DEBUG
Section {
Button(String("Reset Feed Welcome Prompt")) {
showFeedWelcomePrompt = true
}
Section {
Button(String("Reset Feed Welcome Prompt")) {
showFeedWelcomePrompt = true
}

Button(String("Create Error")) {
handleError(ApiClientError.insufficientPermissions)
Button(String("Create Error")) {
handleError(ApiClientError.insufficientPermissions)
}
} header: {
Text(verbatim: "Debug Tools")
}
} header: {
Text(verbatim: "Debug Tools")
}
#endif
}
.navigationTitle("Developer")
Expand Down
1 change: 1 addition & 0 deletions Mlem/App/Views/Shared/MarkdownTextEditor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ struct MarkdownTextEditor<Content: View>: UIViewRepresentable {
}

func textViewDidBeginEditing(_ textView: UITextView) {
parent.placeholderLabel.isHidden = !textView.text.isEmpty
parent.onBeginEditing()
}
}
Expand Down
Loading

0 comments on commit 31e9a22

Please sign in to comment.