mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-11-01 07:53:26 +00:00
Merge 6672f364b0bd6cb03c211195cce2483a35c4298d into 9b84b361225facd8e623f25d76f889d3dc54a879
This commit is contained in:
commit
8cd30351f3
@ -67,6 +67,15 @@ final class ConversationUIModel: ObservableObject {
|
||||
chatViewModel.sendMessage("/clear")
|
||||
}
|
||||
|
||||
/// Resends a message's content as a fresh outgoing private message to
|
||||
/// `peerID`, regardless of which conversation (if any) is currently open.
|
||||
/// Goes straight through the same send path a normal DM uses -- this is
|
||||
/// not a distinct wire concept, just a new outgoing message that happens
|
||||
/// to start from someone else's words.
|
||||
func forwardMessage(_ content: String, to peerID: PeerID) {
|
||||
chatViewModel.sendPrivateMessage(content, to: peerID)
|
||||
}
|
||||
|
||||
func sendHug(to sender: String) {
|
||||
chatViewModel.sendMessage("/hug @\(sender)")
|
||||
}
|
||||
|
||||
@ -65,6 +65,8 @@ final class PeerListModel: ObservableObject {
|
||||
@Published private(set) var visibleGeohashPeerCount = 0
|
||||
@Published private(set) var renderID = ""
|
||||
|
||||
var activeChannel: ChannelID { conversations.activeChannel }
|
||||
|
||||
private let chatViewModel: ChatViewModel
|
||||
private let conversations: ConversationStore
|
||||
private let locationChannelsModel: LocationChannelsModel
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
151
bitchat/Views/ForwardMessageSheet.swift
Normal file
151
bitchat/Views/ForwardMessageSheet.swift
Normal file
@ -0,0 +1,151 @@
|
||||
//
|
||||
// ForwardMessageSheet.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import BitFoundation
|
||||
import SwiftUI
|
||||
|
||||
/// Picker presented from a message's context menu: choose a direct
|
||||
/// conversation to resend this message's content into. Deliberately DMs
|
||||
/// only for v1 -- forwarding into a public mesh/geohash channel is a
|
||||
/// materially different action (broadcasting someone's content rather than
|
||||
/// resending it privately) and stays out of scope here.
|
||||
///
|
||||
/// Sources targets the same way the people sheet does: currently-known mesh
|
||||
/// peers first (PeerListModel.meshRows, connected or not), then people whose
|
||||
/// direct conversation fell out of that roster (PeerListModel.recentChatRows,
|
||||
/// reusing RecentChatList verbatim -- see its own doc comment for why that
|
||||
/// section exists). "Me" and blocked peers are never valid forward targets.
|
||||
struct ForwardMessageSheet: View {
|
||||
@EnvironmentObject private var peerListModel: PeerListModel
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@ThemedPalette private var palette
|
||||
|
||||
/// Called with the chosen recipient; the sheet dismisses itself right
|
||||
/// after, so this only needs to hand off the target.
|
||||
let onForward: (PeerID) -> Void
|
||||
|
||||
private enum Strings {
|
||||
static let title = String(localized: "forward.title", defaultValue: "Forward to…", comment: "Title of the sheet for picking who to forward a message to")
|
||||
static let noRecipients = String(localized: "forward.no_recipients", defaultValue: "No one to forward to yet — no known peers or recent conversations.", comment: "Empty state shown in the forward-message sheet when there is nobody to pick")
|
||||
static let peersHeader = String(localized: "forward.peers_header", defaultValue: "people", comment: "Section header above known peers in the forward-message sheet")
|
||||
static let accessibilityHint = String(localized: "forward.accessibility.forward_hint", defaultValue: "forwards message to this conversation", comment: "Accessibility hint for selecting a target to forward a message to")
|
||||
}
|
||||
|
||||
private var meshTargets: [MeshPeerRow] {
|
||||
peerListModel.meshRows.filter { !$0.isMe && !$0.isBlocked }
|
||||
}
|
||||
|
||||
private var filteredRecentChatRows: [RecentChatRow] {
|
||||
peerListModel.recentChatRows.filter { row in
|
||||
if case .location = peerListModel.activeChannel {
|
||||
return true
|
||||
} else {
|
||||
return !row.peerID.isGeoDM
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var hasAnyTarget: Bool {
|
||||
!meshTargets.isEmpty || !filteredRecentChatRows.isEmpty
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text(Strings.title)
|
||||
.bitchatFont(size: 16, weight: .bold)
|
||||
.foregroundColor(palette.primary)
|
||||
Spacer()
|
||||
SheetCloseButton { dismiss() }
|
||||
.foregroundColor(palette.primary)
|
||||
}
|
||||
.padding()
|
||||
.themedSurface(opacity: 0.95)
|
||||
|
||||
content
|
||||
}
|
||||
.frame(width: 380, height: 420)
|
||||
.themedSheetBackground()
|
||||
#else
|
||||
NavigationView {
|
||||
content
|
||||
.themedSheetBackground()
|
||||
.navigationTitle(Text(Strings.title))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
SheetCloseButton { dismiss() }
|
||||
.foregroundColor(palette.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
if !hasAnyTarget {
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(Strings.noRecipients)
|
||||
.bitchatFont(size: 14)
|
||||
.foregroundColor(palette.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 24)
|
||||
Spacer()
|
||||
}
|
||||
} else {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if !meshTargets.isEmpty {
|
||||
PeopleSectionHeader(
|
||||
icon: "antenna.radiowaves.left.and.right",
|
||||
iconColor: palette.accentBlue,
|
||||
title: Strings.peersHeader
|
||||
)
|
||||
ForEach(meshTargets) { peer in
|
||||
forwardRow(displayName: peer.displayName, peerID: peer.peerID)
|
||||
}
|
||||
}
|
||||
|
||||
RecentChatList(
|
||||
chats: filteredRecentChatRows,
|
||||
accessibilityHint: Strings.accessibilityHint
|
||||
) { peerID in
|
||||
forward(to: peerID)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func forwardRow(displayName: String, peerID: PeerID) -> some View {
|
||||
Button {
|
||||
forward(to: peerID)
|
||||
} label: {
|
||||
Text(verbatim: displayName)
|
||||
.bitchatFont(size: 14)
|
||||
.foregroundColor(palette.primary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityHint(Strings.accessibilityHint)
|
||||
}
|
||||
|
||||
private func forward(to peerID: PeerID) {
|
||||
onForward(peerID)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
@ -40,6 +40,7 @@ struct MessageListView: View {
|
||||
|
||||
@State private var showMessageActions = false
|
||||
@State private var showClearConfirmation = false
|
||||
@State private var messageToForward: BitchatMessage?
|
||||
@State private var lastScrollTime: Date = .distantPast
|
||||
@State private var scrollThrottleTimer: Timer?
|
||||
@State private var unseenCount = 0
|
||||
@ -157,6 +158,17 @@ struct MessageListView: View {
|
||||
conversationUIModel.resendFailedPrivateMessage(message)
|
||||
}
|
||||
}
|
||||
if conversationUIModel.mediaAttachment(for: message) == nil {
|
||||
Button(
|
||||
String(
|
||||
localized: "content.actions.forward",
|
||||
defaultValue: "Forward",
|
||||
comment: "Context menu action that opens a picker to forward this message to another conversation"
|
||||
)
|
||||
) {
|
||||
messageToForward = message
|
||||
}
|
||||
}
|
||||
if showsUserActions {
|
||||
Button("content.actions.block", role: .destructive) {
|
||||
conversationUIModel.block(peerID: message.senderPeerID, displayName: message.sender)
|
||||
@ -273,6 +285,16 @@ struct MessageListView: View {
|
||||
.onDisappear {
|
||||
scrollThrottleTimer?.invalidate()
|
||||
}
|
||||
.sheet(isPresented: Binding(
|
||||
get: { messageToForward != nil },
|
||||
set: { isPresented in if !isPresented { messageToForward = nil } }
|
||||
)) {
|
||||
if let messageToForward {
|
||||
ForwardMessageSheet { peerID in
|
||||
conversationUIModel.forwardMessage(messageToForward.content, to: peerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ struct RecentChatList: View {
|
||||
@ThemedPalette private var palette
|
||||
|
||||
let chats: [RecentChatRow]
|
||||
var accessibilityHint: String? = nil
|
||||
let onTapChat: (PeerID) -> Void
|
||||
|
||||
private enum Strings {
|
||||
@ -72,7 +73,7 @@ struct RecentChatList: View {
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(accessibilityDescription(for: chat))
|
||||
.accessibilityAddTraits(.isButton)
|
||||
.accessibilityHint(Strings.openChatHint)
|
||||
.accessibilityHint(accessibilityHint ?? Strings.openChatHint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user