mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-29 07:27:16 +00:00
Compare commits
5 Commits
fa64e8cb67
...
e0012711de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0012711de | ||
|
|
f8239de844 | ||
|
|
6a1475e98a | ||
|
|
0a4a5a5cc5 | ||
|
|
7bea37c92f |
@ -39953,6 +39953,192 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.message.quote" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "اقتباس في المسودة"
|
||||
}
|
||||
},
|
||||
"bn" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "কম্পোজারে উদ্ধৃতি"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "in entwurf zitieren"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "quote in composer"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "citar en el compositor"
|
||||
}
|
||||
},
|
||||
"fa" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "نقلقول در پیشنویس"
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "quote sa composer"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "citer dans le compositeur"
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ציטוט בטיוטה"
|
||||
}
|
||||
},
|
||||
"hi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "कंपोज़र में उद्धृत करें"
|
||||
}
|
||||
},
|
||||
"id" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "kutip di composer"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "cita nel compositore"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "入力欄に引用"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "작성창에 인용"
|
||||
}
|
||||
},
|
||||
"ms" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "petik dalam composer"
|
||||
}
|
||||
},
|
||||
"ne" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "कम्पोजरमा उद्धरण"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "citeren in opsteller"
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "cytuj w polu wpisu"
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "citar no compositor"
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "citar no compositor"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "цитировать в поле ввода"
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "citera i skrivfältet"
|
||||
}
|
||||
},
|
||||
"ta" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "எழுதும் இடத்தில் மேற்கோள்"
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "อ้างในช่องพิมพ์"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "yazma alanına alıntıla"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "цитувати у полі вводу"
|
||||
}
|
||||
},
|
||||
"ur" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "کمپوزر میں اقتباس"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "trích dẫn vào ô soạn"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "引用到输入框"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "引用到輸入框"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"content.message.show_less" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
|
||||
62
bitchat/Services/MessageClipboard.swift
Normal file
62
bitchat/Services/MessageClipboard.swift
Normal file
@ -0,0 +1,62 @@
|
||||
//
|
||||
// MessageClipboard.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
/// Clipboard helpers for message actions. Kept out of the view so unit tests
|
||||
/// can assert quote formatting without standing up SwiftUI.
|
||||
enum MessageClipboard {
|
||||
/// Plain message body for the pasteboard.
|
||||
static func copyPlaintext(_ content: String) {
|
||||
#if os(iOS)
|
||||
UIPasteboard.general.string = content
|
||||
#elseif os(macOS)
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.setString(content, forType: .string)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Builds a composer-ready quote block (`> line` per line, trailing blank).
|
||||
/// Sender header is plain text (`> alice:`), not `@alice`, so quoting does
|
||||
/// not re-fire mention notifications for the quoted person or for names
|
||||
/// inside the quoted body (those stay behind `> ` and are not live tokens).
|
||||
static func quoteForComposer(_ content: String, sender: String?) -> String {
|
||||
let body = content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !body.isEmpty else { return "" }
|
||||
let header: String
|
||||
if let sender, !sender.isEmpty, sender != "system" {
|
||||
header = "> \(sender):\n"
|
||||
} else {
|
||||
header = ""
|
||||
}
|
||||
let quoted = body
|
||||
.split(separator: "\n", omittingEmptySubsequences: false)
|
||||
.map { "> \($0)" }
|
||||
.joined(separator: "\n")
|
||||
return header + quoted + "\n\n"
|
||||
}
|
||||
|
||||
/// Appends a quote to an existing composer draft without wiping it.
|
||||
/// Only a trailing newline skips inserting another separator — a trailing
|
||||
/// space must still get a newline so the quote marker starts its own line.
|
||||
static func appendQuote(to draft: String, content: String, sender: String?) -> String {
|
||||
let quote = quoteForComposer(content, sender: sender)
|
||||
guard !quote.isEmpty else { return draft }
|
||||
if draft.isEmpty { return quote }
|
||||
if draft.hasSuffix("\n") {
|
||||
return draft + quote
|
||||
}
|
||||
return draft + "\n" + quote
|
||||
}
|
||||
}
|
||||
@ -251,12 +251,17 @@ struct ContentView: View {
|
||||
.frame(minWidth: 600, minHeight: 400)
|
||||
#endif
|
||||
.onChange(of: selectedPrivatePeerID) { newValue in
|
||||
// Shared composer draft must not travel across conversation
|
||||
// targets (quoting a DM then opening public would otherwise
|
||||
// one-tap-broadcast private content).
|
||||
messageText = ""
|
||||
if newValue != nil {
|
||||
showSidebar = true
|
||||
}
|
||||
sharedContentImportModel.updateDestination(sharedContentDestination)
|
||||
}
|
||||
.onChange(of: locationChannelsModel.selectedChannel) { _ in
|
||||
messageText = ""
|
||||
sharedContentImportModel.updateDestination(sharedContentDestination)
|
||||
}
|
||||
.sheet(
|
||||
|
||||
@ -143,13 +143,23 @@ struct MessageListView: View {
|
||||
}
|
||||
}
|
||||
Button("content.message.copy") {
|
||||
#if os(iOS)
|
||||
UIPasteboard.general.string = message.content
|
||||
#else
|
||||
let pb = NSPasteboard.general
|
||||
pb.clearContents()
|
||||
pb.setString(message.content, forType: .string)
|
||||
#endif
|
||||
MessageClipboard.copyPlaintext(message.content)
|
||||
}
|
||||
if !message.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||
message.sender != "system" {
|
||||
Button {
|
||||
messageText = MessageClipboard.appendQuote(
|
||||
to: messageText,
|
||||
content: message.content,
|
||||
sender: message.sender
|
||||
)
|
||||
} label: {
|
||||
Text(String(
|
||||
localized: "content.message.quote",
|
||||
defaultValue: "quote in composer",
|
||||
comment: "Context menu action that inserts a quoted copy of the message into the composer draft"
|
||||
))
|
||||
}
|
||||
}
|
||||
if isResendableFailedMessage(message) {
|
||||
Button("content.actions.resend") {
|
||||
|
||||
46
bitchatTests/Services/MessageClipboardTests.swift
Normal file
46
bitchatTests/Services/MessageClipboardTests.swift
Normal file
@ -0,0 +1,46 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import bitchat
|
||||
|
||||
struct MessageClipboardTests {
|
||||
@Test func quoteWrapsEachLineAndAddsSender() {
|
||||
let quoted = MessageClipboard.quoteForComposer("hello\nworld", sender: "alice")
|
||||
#expect(quoted == "> alice:\n> hello\n> world\n\n")
|
||||
}
|
||||
|
||||
@Test func quoteSkipsSystemSenderHeader() {
|
||||
let quoted = MessageClipboard.quoteForComposer("joined", sender: "system")
|
||||
#expect(quoted == "> joined\n\n")
|
||||
}
|
||||
|
||||
@Test func emptyContentYieldsEmptyQuote() {
|
||||
#expect(MessageClipboard.quoteForComposer(" ", sender: "alice").isEmpty)
|
||||
}
|
||||
|
||||
@Test func appendQuotePreservesExistingDraft() {
|
||||
let result = MessageClipboard.appendQuote(
|
||||
to: "already typing",
|
||||
content: "prior message",
|
||||
sender: "bob"
|
||||
)
|
||||
#expect(result == "already typing\n> bob:\n> prior message\n\n")
|
||||
}
|
||||
|
||||
@Test func appendQuoteOntoEmptyDraft() {
|
||||
let result = MessageClipboard.appendQuote(
|
||||
to: "",
|
||||
content: "solo",
|
||||
sender: "carol"
|
||||
)
|
||||
#expect(result == "> carol:\n> solo\n\n")
|
||||
}
|
||||
|
||||
@Test func appendQuoteAfterTrailingSpaceStartsNewLine() {
|
||||
let result = MessageClipboard.appendQuote(
|
||||
to: "draft with space ",
|
||||
content: "quoted",
|
||||
sender: "dana"
|
||||
)
|
||||
#expect(result == "draft with space \n> dana:\n> quoted\n\n")
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user