Compare commits
9 Commits
av/prepare
...
av/desktop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95f4a8c906 | ||
|
|
71d6410604 | ||
|
|
141611293f | ||
|
|
c9400fe932 | ||
|
|
445a8e75fe | ||
|
|
9d30a3495e | ||
|
|
d77980e50e | ||
|
|
bb02f07370 | ||
|
|
a3cd7ca89e |
@@ -171,6 +171,12 @@ func apiSetUserContactReceipts(_ userId: Int64, userMsgReceiptSettings: UserMsgR
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetUserGroupReceipts(_ userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) async throws {
|
||||
let r = await chatSendCmd(.apiSetUserGroupReceipts(userId: userId, userMsgReceiptSettings: userMsgReceiptSettings))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiHideUser(_ userId: Int64, viewPwd: String) async throws -> User {
|
||||
try await setUserPrivacy_(.apiHideUser(userId: userId, viewPwd: viewPwd))
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ struct CIFileView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let sentFile: ChatItem = ChatItem(
|
||||
chatDir: .directSnd,
|
||||
meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true),
|
||||
meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true),
|
||||
content: .sndMsgContent(msgContent: .file("")),
|
||||
quotedItem: nil,
|
||||
file: CIFile.getSample(fileStatus: .sndComplete)
|
||||
|
||||
@@ -13,6 +13,7 @@ struct CIMetaView: View {
|
||||
@EnvironmentObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var metaColor = Color.secondary
|
||||
var paleMetaColor = Color(UIColor.tertiaryLabel)
|
||||
|
||||
var body: some View {
|
||||
if chatItem.isDeletedContent {
|
||||
@@ -21,12 +22,23 @@ struct CIMetaView: View {
|
||||
let meta = chatItem.meta
|
||||
let ttl = chat.chatInfo.timedMessagesTTL
|
||||
switch meta.itemStatus {
|
||||
case .sndSent:
|
||||
ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .sent)
|
||||
case .sndRcvd:
|
||||
ZStack {
|
||||
ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd1)
|
||||
ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd2)
|
||||
case let .sndSent(sndProgress):
|
||||
switch sndProgress {
|
||||
case .complete: ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .sent)
|
||||
case .partial: ciMetaText(meta, chatTTL: ttl, color: paleMetaColor, sent: .sent)
|
||||
}
|
||||
case let .sndRcvd(_, sndProgress):
|
||||
switch sndProgress {
|
||||
case .complete:
|
||||
ZStack {
|
||||
ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd1)
|
||||
ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd2)
|
||||
}
|
||||
case .partial:
|
||||
ZStack {
|
||||
ciMetaText(meta, chatTTL: ttl, color: paleMetaColor, sent: .rcvd1)
|
||||
ciMetaText(meta, chatTTL: ttl, color: paleMetaColor, sent: .rcvd2)
|
||||
}
|
||||
}
|
||||
default:
|
||||
ciMetaText(meta, chatTTL: ttl, color: metaColor)
|
||||
@@ -61,7 +73,7 @@ func ciMetaText(_ meta: CIMeta, chatTTL: Int?, color: Color = .clear, transparen
|
||||
switch sent {
|
||||
case nil: r = r + t1
|
||||
case .sent: r = r + t1 + gap
|
||||
case .rcvd1: r = r + t.foregroundColor(transparent ? .clear : color.opacity(0.67)) + gap
|
||||
case .rcvd1: r = r + t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) + gap
|
||||
case .rcvd2: r = r + gap + t1
|
||||
}
|
||||
r = r + Text(" ")
|
||||
@@ -78,8 +90,12 @@ private func statusIconText(_ icon: String, _ color: Color) -> Text {
|
||||
struct CIMetaView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent))
|
||||
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, itemEdited: true))
|
||||
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete)))
|
||||
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial)))
|
||||
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete)))
|
||||
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial)))
|
||||
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete)))
|
||||
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true))
|
||||
CIMetaView(chatItem: ChatItem.getDeletedContentSample())
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 100))
|
||||
|
||||
@@ -268,7 +268,7 @@ struct CIVoiceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let sentVoiceMessage: ChatItem = ChatItem(
|
||||
chatDir: .directSnd,
|
||||
meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true),
|
||||
meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true),
|
||||
content: .sndMsgContent(msgContent: .voice(text: "", duration: 30)),
|
||||
quotedItem: nil,
|
||||
file: CIFile.getSample(fileStatus: .sndComplete)
|
||||
|
||||
@@ -32,7 +32,7 @@ func emojiText(_ text: String) -> Text {
|
||||
struct EmojiItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group{
|
||||
EmojiItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent))
|
||||
EmojiItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete)))
|
||||
EmojiItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "👍"))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 70))
|
||||
|
||||
@@ -75,14 +75,14 @@ struct FramedCIVoiceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let sentVoiceMessage: ChatItem = ChatItem(
|
||||
chatDir: .directSnd,
|
||||
meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true),
|
||||
meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true),
|
||||
content: .sndMsgContent(msgContent: .voice(text: "Hello there", duration: 30)),
|
||||
quotedItem: nil,
|
||||
file: CIFile.getSample(fileStatus: .sndComplete)
|
||||
)
|
||||
let voiceMessageWithQuote: ChatItem = ChatItem(
|
||||
chatDir: .directSnd,
|
||||
meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true),
|
||||
meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true),
|
||||
content: .sndMsgContent(msgContent: .voice(text: "", duration: 30)),
|
||||
quotedItem: CIQuote.getSample(1, .now, "Hi", chatDir: .directRcv),
|
||||
file: CIFile.getSample(fileStatus: .sndComplete)
|
||||
|
||||
@@ -349,8 +349,8 @@ struct FramedItemView_Previews: PreviewProvider {
|
||||
Group{
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
@@ -363,10 +363,10 @@ struct FramedItemView_Previews: PreviewProvider {
|
||||
struct FramedItemView_Edited_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
@@ -381,10 +381,10 @@ struct FramedItemView_Edited_Previews: PreviewProvider {
|
||||
struct FramedItemView_Deleted_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil))
|
||||
|
||||
@@ -46,7 +46,7 @@ struct MarkedDeletedItemView: View {
|
||||
struct MarkedDeletedItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
MarkedDeletedItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now)))
|
||||
MarkedDeletedItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 200))
|
||||
}
|
||||
|
||||
@@ -14,11 +14,23 @@ struct ChatItemInfoView: View {
|
||||
var ci: ChatItem
|
||||
@Binding var chatItemInfo: ChatItemInfo?
|
||||
@State private var selection: CIInfoTab = .history
|
||||
@State private var alert: CIInfoViewAlert? = nil
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
|
||||
enum CIInfoTab {
|
||||
case history
|
||||
case quote
|
||||
case delivery
|
||||
}
|
||||
|
||||
enum CIInfoViewAlert: Identifiable {
|
||||
case deliveryStatusAlert(status: CIStatus)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .deliveryStatusAlert: return "deliveryStatusAlert"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -31,6 +43,11 @@ struct ChatItemInfoView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert(item: $alert) { alertItem in
|
||||
switch(alertItem) {
|
||||
case let .deliveryStatusAlert(status): return deliveryStatusAlert(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,19 +57,44 @@ struct ChatItemInfoView: View {
|
||||
: NSLocalizedString("Received message", comment: "message info title")
|
||||
}
|
||||
|
||||
private var numTabs: Int {
|
||||
var numTabs = 1
|
||||
if chatItemInfo?.memberDeliveryStatuses != nil {
|
||||
numTabs += 1
|
||||
}
|
||||
if ci.quotedItem != nil {
|
||||
numTabs += 1
|
||||
}
|
||||
return numTabs
|
||||
}
|
||||
|
||||
@ViewBuilder private func itemInfoView() -> some View {
|
||||
if let qi = ci.quotedItem {
|
||||
if numTabs > 1 {
|
||||
TabView(selection: $selection) {
|
||||
if let mdss = chatItemInfo?.memberDeliveryStatuses {
|
||||
deliveryTab(mdss)
|
||||
.tabItem {
|
||||
Label("Delivery", systemImage: "checkmark.message")
|
||||
}
|
||||
.tag(CIInfoTab.delivery)
|
||||
}
|
||||
historyTab()
|
||||
.tabItem {
|
||||
Label("History", systemImage: "clock")
|
||||
}
|
||||
.tag(CIInfoTab.history)
|
||||
quoteTab(qi)
|
||||
.tabItem {
|
||||
Label("In reply to", systemImage: "arrowshape.turn.up.left")
|
||||
}
|
||||
.tag(CIInfoTab.quote)
|
||||
if let qi = ci.quotedItem {
|
||||
quoteTab(qi)
|
||||
.tabItem {
|
||||
Label("In reply to", systemImage: "arrowshape.turn.up.left")
|
||||
}
|
||||
.tag(CIInfoTab.quote)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if chatItemInfo?.memberDeliveryStatuses != nil {
|
||||
selection = .delivery
|
||||
}
|
||||
}
|
||||
} else {
|
||||
historyTab()
|
||||
@@ -217,9 +259,89 @@ struct ChatItemInfoView: View {
|
||||
: Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
}
|
||||
|
||||
@ViewBuilder private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
details()
|
||||
Divider().padding(.vertical)
|
||||
Text("Delivery")
|
||||
.font(.title2)
|
||||
.padding(.bottom, 4)
|
||||
memberDeliveryStatusesView(memberDeliveryStatuses)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
|
||||
@ViewBuilder private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
let mss = membersStatuses(memberDeliveryStatuses)
|
||||
if !mss.isEmpty {
|
||||
ForEach(mss, id: \.0.groupMemberId) { memberStatus in
|
||||
memberDeliveryStatusView(memberStatus.0, memberStatus.1)
|
||||
}
|
||||
} else {
|
||||
Text("No info on delivery")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] {
|
||||
memberDeliveryStatuses.compactMap({ mds in
|
||||
if let mem = ChatModel.shared.groupMembers.first(where: { $0.groupMemberId == mds.groupMemberId }) {
|
||||
return (mem, mds.memberDeliveryStatus)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus) -> some View {
|
||||
HStack{
|
||||
ProfileImage(imageStr: member.image)
|
||||
.frame(width: 30, height: 30)
|
||||
.padding(.trailing, 2)
|
||||
Text(member.chatViewName)
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
Group {
|
||||
if let (icon, statusColor) = status.statusIcon(Color.secondary) {
|
||||
switch status {
|
||||
case .sndRcvd:
|
||||
ZStack(alignment: .trailing) {
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(statusColor.opacity(0.67))
|
||||
.padding(.trailing, 6)
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(statusColor.opacity(0.67))
|
||||
}
|
||||
default:
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(statusColor)
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "ellipsis")
|
||||
.foregroundColor(Color.secondary)
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
alert = .deliveryStatusAlert(status: status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deliveryStatusAlert(_ status: CIStatus) -> Alert {
|
||||
Alert(
|
||||
title: Text(status.statusText),
|
||||
message: Text(status.statusDescription)
|
||||
)
|
||||
}
|
||||
|
||||
private func itemInfoShareText() -> String {
|
||||
let meta = ci.meta
|
||||
var shareText: [String] = [title, ""]
|
||||
var shareText: [String] = [String.localizedStringWithFormat(NSLocalizedString("# %@", comment: "copied message info title, # <title>"), title), ""]
|
||||
shareText += [String.localizedStringWithFormat(NSLocalizedString("Sent at: %@", comment: "copied message info"), localTimestamp(meta.itemTs))]
|
||||
if !ci.chatDir.sent {
|
||||
shareText += [String.localizedStringWithFormat(NSLocalizedString("Received at: %@", comment: "copied message info"), localTimestamp(meta.createdAt))]
|
||||
@@ -245,7 +367,7 @@ struct ChatItemInfoView: View {
|
||||
]
|
||||
}
|
||||
if let qi = ci.quotedItem {
|
||||
shareText += ["", NSLocalizedString("In reply to", comment: "copied message info")]
|
||||
shareText += ["", NSLocalizedString("## In reply to", comment: "copied message info")]
|
||||
let t = qi.text
|
||||
shareText += [""]
|
||||
if let sender = qi.getSender(nil) {
|
||||
@@ -262,9 +384,23 @@ struct ChatItemInfoView: View {
|
||||
}
|
||||
shareText += [t != "" ? t : NSLocalizedString("no text", comment: "copied message info in history")]
|
||||
}
|
||||
if let mdss = chatItemInfo?.memberDeliveryStatuses {
|
||||
let mss = membersStatuses(mdss)
|
||||
if !mss.isEmpty {
|
||||
shareText += ["", NSLocalizedString("## Delivery", comment: "copied message info")]
|
||||
shareText += [""]
|
||||
for (member, status) in mss {
|
||||
shareText += [String.localizedStringWithFormat(
|
||||
NSLocalizedString("%@: %@", comment: "copied message info, <recipient>: <message delivery status description>"),
|
||||
member.chatViewName,
|
||||
status.statusDescription
|
||||
)]
|
||||
}
|
||||
}
|
||||
}
|
||||
if let chatItemInfo = chatItemInfo,
|
||||
!chatItemInfo.itemVersions.isEmpty {
|
||||
shareText += ["", NSLocalizedString("History", comment: "copied message info")]
|
||||
shareText += ["", NSLocalizedString("## History", comment: "copied message info")]
|
||||
for (index, itemVersion) in chatItemInfo.itemVersions.enumerated() {
|
||||
let t = itemVersion.msgContent.text
|
||||
shareText += [
|
||||
|
||||
@@ -125,9 +125,9 @@ struct ChatItemView_Previews: PreviewProvider {
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), revealed: Binding.constant(false))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), revealed: Binding.constant(false))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getDeletedContentSample(), revealed: Binding.constant(false))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent, itemLive: true), revealed: Binding.constant(true))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemLive: true), revealed: Binding.constant(true))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true))
|
||||
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 70))
|
||||
.environmentObject(Chat.sampleData)
|
||||
|
||||
@@ -770,6 +770,12 @@ struct ChatView: View {
|
||||
await MainActor.run {
|
||||
chatItemInfo = ciInfo
|
||||
}
|
||||
if case let .group(gInfo) = chat.chatInfo {
|
||||
let groupMembers = await apiListMembers(gInfo.groupId)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.groupMembers = groupMembers
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiGetChatItemInfo error: \(responseError(error))")
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
let SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
|
||||
|
||||
struct GroupChatInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@@ -21,6 +23,8 @@ struct GroupChatInfoView: View {
|
||||
@State private var showAddMembersSheet: Bool = false
|
||||
@State private var connectionStats: ConnectionStats?
|
||||
@State private var connectionCode: String?
|
||||
@State private var sendReceipts = SendReceipts.userDefault(true)
|
||||
@State private var sendReceiptsUserDefault = true
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@State private var searchText: String = ""
|
||||
@FocusState private var searchFocussed
|
||||
@@ -30,6 +34,7 @@ struct GroupChatInfoView: View {
|
||||
case clearChatAlert
|
||||
case leaveGroupAlert
|
||||
case cantInviteIncognitoAlert
|
||||
case largeGroupReceiptsDisabled
|
||||
|
||||
var id: GroupChatInfoViewAlert { get { self } }
|
||||
}
|
||||
@@ -52,6 +57,11 @@ struct GroupChatInfoView: View {
|
||||
addOrEditWelcomeMessage()
|
||||
}
|
||||
groupPreferencesButton($groupInfo)
|
||||
if members.filter { $0.memberCurrent }.count <= SMALL_GROUPS_RCPS_MEM_LIMIT {
|
||||
sendReceiptsOption()
|
||||
} else {
|
||||
sendReceiptsOptionDisabled()
|
||||
}
|
||||
} header: {
|
||||
Text("")
|
||||
} footer: {
|
||||
@@ -115,9 +125,14 @@ struct GroupChatInfoView: View {
|
||||
case .clearChatAlert: return clearChatAlert()
|
||||
case .leaveGroupAlert: return leaveGroupAlert()
|
||||
case .cantInviteIncognitoAlert: return cantInviteIncognitoAlert()
|
||||
case .largeGroupReceiptsDisabled: return largeGroupReceiptsDisabledAlert()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if let currentUser = chatModel.currentUser {
|
||||
sendReceiptsUserDefault = currentUser.sendRcptsSmallGroups
|
||||
}
|
||||
sendReceipts = SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, userDefault: sendReceiptsUserDefault)
|
||||
do {
|
||||
if let link = try apiGetGroupLink(groupInfo.groupId) {
|
||||
(groupLink, groupLinkMemberRole) = link
|
||||
@@ -328,6 +343,38 @@ struct GroupChatInfoView: View {
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
|
||||
private func sendReceiptsOption() -> some View {
|
||||
Picker(selection: $sendReceipts) {
|
||||
ForEach([.yes, .no, .userDefault(sendReceiptsUserDefault)]) { (opt: SendReceipts) in
|
||||
Text(opt.text)
|
||||
}
|
||||
} label: {
|
||||
Label("Send receipts", systemImage: "checkmark.message")
|
||||
}
|
||||
.frame(height: 36)
|
||||
.onChange(of: sendReceipts) { _ in
|
||||
setSendReceipts()
|
||||
}
|
||||
}
|
||||
|
||||
private func setSendReceipts() {
|
||||
var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults
|
||||
chatSettings.sendRcpts = sendReceipts.bool()
|
||||
updateChatSettings(chat, chatSettings: chatSettings)
|
||||
}
|
||||
|
||||
private func sendReceiptsOptionDisabled() -> some View {
|
||||
HStack {
|
||||
Label("Send receipts", systemImage: "checkmark.message")
|
||||
Spacer()
|
||||
Text("disabled")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.onTapGesture {
|
||||
alert = .largeGroupReceiptsDisabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bool = false) -> some View {
|
||||
@@ -356,6 +403,13 @@ func cantInviteIncognitoAlert() -> Alert {
|
||||
)
|
||||
}
|
||||
|
||||
func largeGroupReceiptsDisabledAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Receipts are disabled"),
|
||||
message: Text("This group has over \(SMALL_GROUPS_RCPS_MEM_LIMIT) members, delivery receipts are not sent.")
|
||||
)
|
||||
}
|
||||
|
||||
struct GroupChatInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GroupChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: GroupInfo.sampleData)
|
||||
|
||||
@@ -258,20 +258,20 @@ struct ChatPreviewView_Previews: PreviewProvider {
|
||||
))
|
||||
ChatPreviewView(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)]
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))]
|
||||
))
|
||||
ChatPreviewView(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)],
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))],
|
||||
chatStats: ChatStats(unreadCount: 11, minUnreadItemId: 0)
|
||||
))
|
||||
ChatPreviewView(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now))]
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))]
|
||||
))
|
||||
ChatPreviewView(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)],
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))],
|
||||
chatStats: ChatStats(unreadCount: 3, minUnreadItemId: 0)
|
||||
))
|
||||
ChatPreviewView(chat: Chat(
|
||||
|
||||
@@ -21,6 +21,10 @@ struct PrivacySettings: View {
|
||||
@State private var contactReceiptsReset = false
|
||||
@State private var contactReceiptsOverrides = 0
|
||||
@State private var contactReceiptsDialogue = false
|
||||
@State private var groupReceipts = false
|
||||
@State private var groupReceiptsReset = false
|
||||
@State private var groupReceiptsOverrides = 0
|
||||
@State private var groupReceiptsDialogue = false
|
||||
@State private var alert: PrivacySettingsViewAlert?
|
||||
|
||||
enum PrivacySettingsViewAlert: Identifiable {
|
||||
@@ -89,15 +93,15 @@ struct PrivacySettings: View {
|
||||
settingsRow("person") {
|
||||
Toggle("Contacts", isOn: $contactReceipts)
|
||||
}
|
||||
// settingsRow("person.2") {
|
||||
// Toggle("Small groups (max 20)", isOn: Binding.constant(false))
|
||||
// }
|
||||
settingsRow("person.2") {
|
||||
Toggle("Small groups (max 20)", isOn: $groupReceipts)
|
||||
}
|
||||
} header: {
|
||||
Text("Send delivery receipts to")
|
||||
} footer: {
|
||||
VStack(alignment: .leading) {
|
||||
Text("These settings are for your current profile **\(ChatModel.shared.currentUser?.displayName ?? "")**.")
|
||||
Text("They can be overridden in contact settings")
|
||||
Text("They can be overridden in contact and group settings.")
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
@@ -113,19 +117,44 @@ struct PrivacySettings: View {
|
||||
contactReceipts.toggle()
|
||||
}
|
||||
}
|
||||
.confirmationDialog(groupReceiptsDialogTitle, isPresented: $groupReceiptsDialogue, titleVisibility: .visible) {
|
||||
Button(groupReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") {
|
||||
setSendReceiptsGroups(groupReceipts, clearOverrides: false)
|
||||
}
|
||||
Button(contactReceipts ? "Enable for all" : "Disable for all", role: .destructive) {
|
||||
setSendReceiptsGroups(groupReceipts, clearOverrides: true)
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
groupReceiptsReset = true
|
||||
groupReceipts.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: contactReceipts) { _ in // sometimes there is race with onAppear
|
||||
.onChange(of: contactReceipts) { _ in
|
||||
if contactReceiptsReset {
|
||||
contactReceiptsReset = false
|
||||
} else {
|
||||
setOrAskSendReceiptsContacts(contactReceipts)
|
||||
}
|
||||
}
|
||||
.onChange(of: groupReceipts) { _ in
|
||||
if groupReceiptsReset {
|
||||
groupReceiptsReset = false
|
||||
} else {
|
||||
setOrAskSendReceiptsGroups(groupReceipts)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if let u = m.currentUser, contactReceipts != u.sendRcptsContacts {
|
||||
contactReceiptsReset = true
|
||||
contactReceipts = u.sendRcptsContacts
|
||||
if let u = m.currentUser {
|
||||
if contactReceipts != u.sendRcptsContacts {
|
||||
contactReceiptsReset = true
|
||||
contactReceipts = u.sendRcptsContacts
|
||||
}
|
||||
if groupReceipts != u.sendRcptsSmallGroups {
|
||||
groupReceiptsReset = true
|
||||
groupReceipts = u.sendRcptsSmallGroups
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert(item: $alert) { alert in
|
||||
@@ -179,7 +208,55 @@ struct PrivacySettings: View {
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
alert = .error(title: "Error setting delivery receipts!", error: "Error: \(responseError(error))")
|
||||
alert = .error(title: "Error setting contact delivery receipts!", error: "Error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setOrAskSendReceiptsGroups(_ enable: Bool) {
|
||||
groupReceiptsOverrides = m.chats.reduce(0) { count, chat in
|
||||
let sendRcpts = chat.chatInfo.groupInfo?.chatSettings.sendRcpts
|
||||
return count + (sendRcpts == nil || sendRcpts == enable ? 0 : 1)
|
||||
}
|
||||
if groupReceiptsOverrides == 0 {
|
||||
setSendReceiptsGroups(enable, clearOverrides: false)
|
||||
} else {
|
||||
groupReceiptsDialogue = true
|
||||
}
|
||||
}
|
||||
|
||||
private var groupReceiptsDialogTitle: LocalizedStringKey {
|
||||
groupReceipts
|
||||
? "Sending receipts is disabled for \(groupReceiptsOverrides) groups"
|
||||
: "Sending receipts is enabled for \(groupReceiptsOverrides) groups"
|
||||
}
|
||||
|
||||
private func setSendReceiptsGroups(_ enable: Bool, clearOverrides: Bool) {
|
||||
Task {
|
||||
do {
|
||||
if let currentUser = m.currentUser {
|
||||
let userMsgReceiptSettings = UserMsgReceiptSettings(enable: enable, clearOverrides: clearOverrides)
|
||||
try await apiSetUserGroupReceipts(currentUser.userId, userMsgReceiptSettings: userMsgReceiptSettings)
|
||||
privacyDeliveryReceiptsSet.set(true)
|
||||
await MainActor.run {
|
||||
var updatedUser = currentUser
|
||||
updatedUser.sendRcptsSmallGroups = enable
|
||||
m.updateUser(updatedUser)
|
||||
if clearOverrides {
|
||||
m.chats.forEach { chat in
|
||||
if var groupInfo = chat.chatInfo.groupInfo {
|
||||
let sendRcpts = groupInfo.chatSettings.sendRcpts
|
||||
if sendRcpts != nil && sendRcpts != enable {
|
||||
groupInfo.chatSettings.sendRcpts = nil
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
alert = .error(title: "Error setting group delivery receipts!", error: "Error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,11 +137,6 @@
|
||||
5CE2BA97284537A800EC33A6 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CE2BA96284537A800EC33A6 /* dummy.m */; };
|
||||
5CE2BA9D284555F500EC33A6 /* SimpleX NSE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 5CDCAD452818589900503DA2 /* SimpleX NSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
5CE2BAA62845617C00EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; platformFilter = ios; };
|
||||
5CE381E12A6C103D004FB9E1 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE381DC2A6C103D004FB9E1 /* libffi.a */; };
|
||||
5CE381E22A6C103D004FB9E1 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE381DD2A6C103D004FB9E1 /* libgmpxx.a */; };
|
||||
5CE381E32A6C103D004FB9E1 /* libHSsimplex-chat-5.2.0.4-HUgQMHMGu1K8FDeUwC5hCd-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE381DE2A6C103D004FB9E1 /* libHSsimplex-chat-5.2.0.4-HUgQMHMGu1K8FDeUwC5hCd-ghc8.10.7.a */; };
|
||||
5CE381E42A6C103D004FB9E1 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE381DF2A6C103D004FB9E1 /* libgmp.a */; };
|
||||
5CE381E52A6C103D004FB9E1 /* libHSsimplex-chat-5.2.0.4-HUgQMHMGu1K8FDeUwC5hCd.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE381E02A6C103D004FB9E1 /* libHSsimplex-chat-5.2.0.4-HUgQMHMGu1K8FDeUwC5hCd.a */; };
|
||||
5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407127ADB1D0007B033A /* Emoji.swift */; };
|
||||
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; };
|
||||
5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; };
|
||||
@@ -176,6 +171,11 @@
|
||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
|
||||
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
|
||||
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
|
||||
64C9F3CF2A73C538002C80AF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C9F3CA2A73C538002C80AF /* libgmpxx.a */; };
|
||||
64C9F3D02A73C538002C80AF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C9F3CB2A73C538002C80AF /* libgmp.a */; };
|
||||
64C9F3D12A73C538002C80AF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C9F3CC2A73C538002C80AF /* libffi.a */; };
|
||||
64C9F3D22A73C538002C80AF /* libHSsimplex-chat-5.3.0.0-FkRHBzksWjH5JbOMv5lWX0-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C9F3CD2A73C538002C80AF /* libHSsimplex-chat-5.3.0.0-FkRHBzksWjH5JbOMv5lWX0-ghc8.10.7.a */; };
|
||||
64C9F3D32A73C538002C80AF /* libHSsimplex-chat-5.3.0.0-FkRHBzksWjH5JbOMv5lWX0.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C9F3CE2A73C538002C80AF /* libHSsimplex-chat-5.3.0.0-FkRHBzksWjH5JbOMv5lWX0.a */; };
|
||||
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
|
||||
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
|
||||
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; };
|
||||
@@ -415,11 +415,6 @@
|
||||
5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = SimpleXChat.docc; sourceTree = "<group>"; };
|
||||
5CE2BA8A2845332200EC33A6 /* SimpleX.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SimpleX.h; sourceTree = "<group>"; };
|
||||
5CE2BA96284537A800EC33A6 /* dummy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = dummy.m; sourceTree = "<group>"; };
|
||||
5CE381DC2A6C103D004FB9E1 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CE381DD2A6C103D004FB9E1 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CE381DE2A6C103D004FB9E1 /* libHSsimplex-chat-5.2.0.4-HUgQMHMGu1K8FDeUwC5hCd-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.4-HUgQMHMGu1K8FDeUwC5hCd-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5CE381DF2A6C103D004FB9E1 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CE381E02A6C103D004FB9E1 /* libHSsimplex-chat-5.2.0.4-HUgQMHMGu1K8FDeUwC5hCd.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.4-HUgQMHMGu1K8FDeUwC5hCd.a"; sourceTree = "<group>"; };
|
||||
5CE4407127ADB1D0007B033A /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = "<group>"; };
|
||||
5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = "<group>"; };
|
||||
5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
|
||||
@@ -454,6 +449,11 @@
|
||||
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
|
||||
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
|
||||
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
|
||||
64C9F3CA2A73C538002C80AF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
64C9F3CB2A73C538002C80AF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
64C9F3CC2A73C538002C80AF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
64C9F3CD2A73C538002C80AF /* libHSsimplex-chat-5.3.0.0-FkRHBzksWjH5JbOMv5lWX0-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.0-FkRHBzksWjH5JbOMv5lWX0-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
64C9F3CE2A73C538002C80AF /* libHSsimplex-chat-5.3.0.0-FkRHBzksWjH5JbOMv5lWX0.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.0-FkRHBzksWjH5JbOMv5lWX0.a"; sourceTree = "<group>"; };
|
||||
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
|
||||
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
|
||||
64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = "<group>"; };
|
||||
@@ -501,13 +501,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5CE381E22A6C103D004FB9E1 /* libgmpxx.a in Frameworks */,
|
||||
64C9F3CF2A73C538002C80AF /* libgmpxx.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5CE381E32A6C103D004FB9E1 /* libHSsimplex-chat-5.2.0.4-HUgQMHMGu1K8FDeUwC5hCd-ghc8.10.7.a in Frameworks */,
|
||||
5CE381E52A6C103D004FB9E1 /* libHSsimplex-chat-5.2.0.4-HUgQMHMGu1K8FDeUwC5hCd.a in Frameworks */,
|
||||
5CE381E12A6C103D004FB9E1 /* libffi.a in Frameworks */,
|
||||
5CE381E42A6C103D004FB9E1 /* libgmp.a in Frameworks */,
|
||||
64C9F3D22A73C538002C80AF /* libHSsimplex-chat-5.3.0.0-FkRHBzksWjH5JbOMv5lWX0-ghc8.10.7.a in Frameworks */,
|
||||
64C9F3D02A73C538002C80AF /* libgmp.a in Frameworks */,
|
||||
64C9F3D12A73C538002C80AF /* libffi.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
64C9F3D32A73C538002C80AF /* libHSsimplex-chat-5.3.0.0-FkRHBzksWjH5JbOMv5lWX0.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -568,11 +568,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CE381DC2A6C103D004FB9E1 /* libffi.a */,
|
||||
5CE381DF2A6C103D004FB9E1 /* libgmp.a */,
|
||||
5CE381DD2A6C103D004FB9E1 /* libgmpxx.a */,
|
||||
5CE381DE2A6C103D004FB9E1 /* libHSsimplex-chat-5.2.0.4-HUgQMHMGu1K8FDeUwC5hCd-ghc8.10.7.a */,
|
||||
5CE381E02A6C103D004FB9E1 /* libHSsimplex-chat-5.2.0.4-HUgQMHMGu1K8FDeUwC5hCd.a */,
|
||||
64C9F3CC2A73C538002C80AF /* libffi.a */,
|
||||
64C9F3CB2A73C538002C80AF /* libgmp.a */,
|
||||
64C9F3CA2A73C538002C80AF /* libgmpxx.a */,
|
||||
64C9F3CD2A73C538002C80AF /* libHSsimplex-chat-5.3.0.0-FkRHBzksWjH5JbOMv5lWX0-ghc8.10.7.a */,
|
||||
64C9F3CE2A73C538002C80AF /* libHSsimplex-chat-5.3.0.0-FkRHBzksWjH5JbOMv5lWX0.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -1478,7 +1478,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 160;
|
||||
CURRENT_PROJECT_VERSION = 161;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1499,7 +1499,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.2;
|
||||
MARKETING_VERSION = 5.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1520,7 +1520,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 160;
|
||||
CURRENT_PROJECT_VERSION = 161;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1541,7 +1541,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.2;
|
||||
MARKETING_VERSION = 5.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1600,7 +1600,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 160;
|
||||
CURRENT_PROJECT_VERSION = 161;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1613,7 +1613,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.2;
|
||||
MARKETING_VERSION = 5.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -1632,7 +1632,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 160;
|
||||
CURRENT_PROJECT_VERSION = 161;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1645,7 +1645,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.2;
|
||||
MARKETING_VERSION = 5.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@@ -19,6 +19,7 @@ public enum ChatCommand {
|
||||
case apiSetActiveUser(userId: Int64, viewPwd: String?)
|
||||
case setAllContactReceipts(enable: Bool)
|
||||
case apiSetUserContactReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings)
|
||||
case apiSetUserGroupReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings)
|
||||
case apiHideUser(userId: Int64, viewPwd: String)
|
||||
case apiUnhideUser(userId: Int64, viewPwd: String)
|
||||
case apiMuteUser(userId: Int64)
|
||||
@@ -128,6 +129,9 @@ public enum ChatCommand {
|
||||
case let .apiSetUserContactReceipts(userId, userMsgReceiptSettings):
|
||||
let umrs = userMsgReceiptSettings
|
||||
return "/_set receipts contacts \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))"
|
||||
case let .apiSetUserGroupReceipts(userId, userMsgReceiptSettings):
|
||||
let umrs = userMsgReceiptSettings
|
||||
return "/_set receipts groups \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))"
|
||||
case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))"
|
||||
case let .apiUnhideUser(userId, viewPwd): return "/_unhide user \(userId) \(encodeJSON(viewPwd))"
|
||||
case let .apiMuteUser(userId): return "/_mute user \(userId)"
|
||||
@@ -257,6 +261,7 @@ public enum ChatCommand {
|
||||
case .apiSetActiveUser: return "apiSetActiveUser"
|
||||
case .setAllContactReceipts: return "setAllContactReceipts"
|
||||
case .apiSetUserContactReceipts: return "apiSetUserContactReceipts"
|
||||
case .apiSetUserGroupReceipts: return "apiSetUserGroupReceipts"
|
||||
case .apiHideUser: return "apiHideUser"
|
||||
case .apiUnhideUser: return "apiUnhideUser"
|
||||
case .apiMuteUser: return "apiMuteUser"
|
||||
|
||||
@@ -1200,6 +1200,13 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
public var groupInfo: GroupInfo? {
|
||||
switch self {
|
||||
case let .group(groupInfo): return groupInfo
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
// this works for features that are common for contacts and groups
|
||||
public func featureEnabled(_ feature: ChatFeature) -> Bool {
|
||||
switch self {
|
||||
@@ -2263,18 +2270,7 @@ public struct CIMeta: Decodable {
|
||||
}
|
||||
|
||||
public func statusIcon(_ metaColor: Color = .secondary) -> (String, Color)? {
|
||||
switch itemStatus {
|
||||
case .sndSent: return ("checkmark", metaColor)
|
||||
case let .sndRcvd(msgRcptStatus):
|
||||
switch msgRcptStatus {
|
||||
case .ok: return ("checkmark", metaColor) // ("checkmark.circle", metaColor)
|
||||
case .badMsgHash: return ("checkmark", .red) // ("checkmark.circle", .red)
|
||||
}
|
||||
case .sndErrorAuth: return ("multiply", .red)
|
||||
case .sndError: return ("exclamationmark.triangle.fill", .yellow)
|
||||
case .rcvNew: return ("circlebadge.fill", Color.accentColor)
|
||||
default: return nil
|
||||
}
|
||||
itemStatus.statusIcon(metaColor)
|
||||
}
|
||||
|
||||
public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, editable: Bool = true) -> CIMeta {
|
||||
@@ -2337,8 +2333,8 @@ private func recent(_ date: Date) -> Bool {
|
||||
|
||||
public enum CIStatus: Decodable {
|
||||
case sndNew
|
||||
case sndSent
|
||||
case sndRcvd(msgRcptStatus: MsgReceiptStatus)
|
||||
case sndSent(sndProgress: SndCIStatusProgress)
|
||||
case sndRcvd(msgRcptStatus: MsgReceiptStatus, sndProgress: SndCIStatusProgress)
|
||||
case sndErrorAuth
|
||||
case sndError(agentError: String)
|
||||
case rcvNew
|
||||
@@ -2355,6 +2351,46 @@ public enum CIStatus: Decodable {
|
||||
case .rcvRead: return "rcvRead"
|
||||
}
|
||||
}
|
||||
|
||||
public func statusIcon(_ metaColor: Color = .secondary) -> (String, Color)? {
|
||||
switch self {
|
||||
case .sndNew: return nil
|
||||
case .sndSent: return ("checkmark", metaColor)
|
||||
case let .sndRcvd(msgRcptStatus, _):
|
||||
switch msgRcptStatus {
|
||||
case .ok: return ("checkmark", metaColor)
|
||||
case .badMsgHash: return ("checkmark", .red)
|
||||
}
|
||||
case .sndErrorAuth: return ("multiply", .red)
|
||||
case .sndError: return ("exclamationmark.triangle.fill", .yellow)
|
||||
case .rcvNew: return ("circlebadge.fill", Color.accentColor)
|
||||
case .rcvRead: return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var statusText: String {
|
||||
switch self {
|
||||
case .sndNew: return NSLocalizedString("Sending message", comment: "item status text")
|
||||
case .sndSent: return NSLocalizedString("Message sent", comment: "item status text")
|
||||
case .sndRcvd: return NSLocalizedString("Sent message received", comment: "item status text")
|
||||
case .sndErrorAuth: return NSLocalizedString("Error sending message", comment: "item status text")
|
||||
case .sndError: return NSLocalizedString("Error sending message", comment: "item status text")
|
||||
case .rcvNew: return NSLocalizedString("Message received", comment: "item status text")
|
||||
case .rcvRead: return NSLocalizedString("Message read", comment: "item status text")
|
||||
}
|
||||
}
|
||||
|
||||
public var statusDescription: String {
|
||||
switch self {
|
||||
case .sndNew: return NSLocalizedString("Sending message is in progress or pending.", comment: "item status description")
|
||||
case .sndSent: return NSLocalizedString("Message has been sent to the recipient's relay.", comment: "item status description")
|
||||
case .sndRcvd: return NSLocalizedString("Message has been received by the recipient.", comment: "item status description")
|
||||
case .sndErrorAuth: return NSLocalizedString("Message delivery error. Most likely this recipient has deleted the connection with you.", comment: "item status description")
|
||||
case let .sndError(agentError): return String.localizedStringWithFormat(NSLocalizedString("Unexpected message delivery error: %@", comment: "item status description"), agentError)
|
||||
case .rcvNew: return NSLocalizedString("New message from this sender.", comment: "item status description")
|
||||
case .rcvRead: return NSLocalizedString("You've read this received message.", comment: "item status description")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum MsgReceiptStatus: String, Decodable {
|
||||
@@ -2362,6 +2398,11 @@ public enum MsgReceiptStatus: String, Decodable {
|
||||
case badMsgHash
|
||||
}
|
||||
|
||||
public enum SndCIStatusProgress: String, Decodable {
|
||||
case partial
|
||||
case complete
|
||||
}
|
||||
|
||||
public enum CIDeleted: Decodable {
|
||||
case deleted(deletedTs: Date?)
|
||||
case moderated(deletedTs: Date?, byGroupMember: GroupMember)
|
||||
@@ -3205,6 +3246,7 @@ public enum ChatItemTTL: Hashable, Identifiable, Comparable {
|
||||
|
||||
public struct ChatItemInfo: Decodable {
|
||||
public var itemVersions: [ChatItemVersion]
|
||||
public var memberDeliveryStatuses: [MemberDeliveryStatus]?
|
||||
}
|
||||
|
||||
public struct ChatItemVersion: Decodable {
|
||||
@@ -3214,3 +3256,8 @@ public struct ChatItemVersion: Decodable {
|
||||
public var itemVersionTs: Date
|
||||
public var createdAt: Date
|
||||
}
|
||||
|
||||
public struct MemberDeliveryStatus: Decodable {
|
||||
public var groupMemberId: Int64
|
||||
public var memberDeliveryStatus: CIStatus
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@ actual fun PlatformTextField(
|
||||
textStyle: MutableState<TextStyle>,
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
onMessageChange: (String) -> Unit
|
||||
onMessageChange: (String) -> Unit,
|
||||
onDone: () -> Unit,
|
||||
) {
|
||||
val cs = composeState.value
|
||||
val textColor = MaterialTheme.colors.onBackground
|
||||
|
||||
@@ -50,4 +50,7 @@ actual fun screenWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp
|
||||
|
||||
actual fun desktopExpandWindowToWidth(width: Dp) {}
|
||||
|
||||
@Composable
|
||||
actual fun allowToShowBackButtonInCenter(): Boolean = true
|
||||
|
||||
actual fun isRtl(text: CharSequence): Boolean = BidiFormatter.getInstance().isRtl(text)
|
||||
|
||||
@@ -274,26 +274,44 @@ fun EndPartOfScreen() {
|
||||
|
||||
@Composable
|
||||
fun DesktopScreen(settingsState: SettingsViewState) {
|
||||
Box {
|
||||
// 56.dp is a size of unused space of settings drawer
|
||||
Box(Modifier.width(DEFAULT_START_MODAL_WIDTH + 56.dp)) {
|
||||
StartPartOfScreen(settingsState)
|
||||
}
|
||||
Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH)) {
|
||||
ModalManager.start.showInView()
|
||||
}
|
||||
Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH).clipToBounds()) {
|
||||
Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) {
|
||||
CenterPartOfScreen()
|
||||
BoxWithConstraints {
|
||||
Box {
|
||||
val maxWidth = this@BoxWithConstraints.maxWidth
|
||||
// 56.dp is a size of unused space of settings drawer
|
||||
val startMaxWidth = when {
|
||||
maxWidth >= DEFAULT_START_MODAL_WIDTH + DEFAULT_MIN_CENTER_MODAL_WIDTH -> DEFAULT_START_MODAL_WIDTH + 56.dp
|
||||
ChatModel.chatId.value == null && !ModalManager.center.hasModalsOpen() -> maxWidth
|
||||
else -> DEFAULT_START_MODAL_WIDTH
|
||||
}
|
||||
if (ModalManager.end.hasModalsOpen()) {
|
||||
VerticalDivider()
|
||||
val startModalMaxWidth = when {
|
||||
maxWidth >= DEFAULT_START_MODAL_WIDTH + DEFAULT_MIN_CENTER_MODAL_WIDTH -> DEFAULT_START_MODAL_WIDTH
|
||||
ChatModel.chatId.value == null && !ModalManager.center.hasModalsOpen() -> maxWidth
|
||||
else -> DEFAULT_START_MODAL_WIDTH
|
||||
}
|
||||
Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH).clipToBounds()) {
|
||||
EndPartOfScreen()
|
||||
Box(Modifier.widthIn(max = startMaxWidth)) {
|
||||
StartPartOfScreen(settingsState)
|
||||
Box(Modifier.widthIn(max = startModalMaxWidth)) {
|
||||
ModalManager.start.showInView()
|
||||
}
|
||||
}
|
||||
val centerMaxWidth = when {
|
||||
maxWidth >= DEFAULT_START_MODAL_WIDTH + DEFAULT_MIN_CENTER_MODAL_WIDTH -> maxWidth - DEFAULT_START_MODAL_WIDTH
|
||||
ChatModel.chatId.value != null || ModalManager.center.hasModalsOpen() -> maxOf(DEFAULT_START_MODAL_WIDTH, maxWidth) - 1.dp
|
||||
else -> 0.dp
|
||||
}
|
||||
Row(Modifier.padding(start = maxOf(maxWidth - centerMaxWidth, 0.dp))) {
|
||||
Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) {
|
||||
CenterPartOfScreen()
|
||||
}
|
||||
if (ModalManager.end.hasModalsOpen() && maxWidth >= DEFAULT_START_MODAL_WIDTH + DEFAULT_MIN_CENTER_MODAL_WIDTH + DEFAULT_END_MODAL_WIDTH) {
|
||||
VerticalDivider()
|
||||
}
|
||||
Box(Modifier.widthIn(max = if (centerMaxWidth < DEFAULT_MIN_CENTER_MODAL_WIDTH + DEFAULT_END_MODAL_WIDTH) centerMaxWidth else DEFAULT_END_MODAL_WIDTH).clipToBounds()) {
|
||||
EndPartOfScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
val (userPickerState, scaffoldState, switchingUsers ) = settingsState
|
||||
val (userPickerState, scaffoldState, switchingUsers) = settingsState
|
||||
val scope = rememberCoroutineScope()
|
||||
if (scaffoldState.drawerState.isOpen) {
|
||||
Box(
|
||||
@@ -306,7 +324,9 @@ fun DesktopScreen(settingsState: SettingsViewState) {
|
||||
})
|
||||
)
|
||||
}
|
||||
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH))
|
||||
if (maxWidth >= DEFAULT_START_MODAL_WIDTH + DEFAULT_MIN_CENTER_MODAL_WIDTH) {
|
||||
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH))
|
||||
}
|
||||
UserPicker(chatModel, userPickerState, switchingUsers) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ object ChatModel {
|
||||
val chatItems = mutableStateListOf<ChatItem>()
|
||||
val groupMembers = mutableStateListOf<GroupMember>()
|
||||
|
||||
val terminalItems = mutableStateListOf<TerminalItem>()
|
||||
val terminalItems = mutableStateOf(emptyList<TerminalItem>())
|
||||
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
|
||||
// Allows to temporary save servers that are being edited on multiple screens
|
||||
val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(null)
|
||||
@@ -484,10 +484,11 @@ object ChatModel {
|
||||
networkStatuses[contact.activeConn.agentConnId] ?: NetworkStatus.Unknown()
|
||||
|
||||
fun addTerminalItem(item: TerminalItem) {
|
||||
if (terminalItems.size >= 500) {
|
||||
terminalItems.removeAt(0)
|
||||
if (terminalItems.value.size >= 500) {
|
||||
terminalItems.value = terminalItems.value.takeLast(499) + item
|
||||
} else {
|
||||
terminalItems.value += item
|
||||
}
|
||||
terminalItems.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1624,18 +1625,12 @@ data class CIMeta (
|
||||
|
||||
val isRcvNew: Boolean get() = itemStatus is CIStatus.RcvNew
|
||||
|
||||
fun statusIcon(primaryColor: Color, metaColor: Color = CurrentColors.value.colors.secondary): Pair<ImageResource, Color>? =
|
||||
when (itemStatus) {
|
||||
is CIStatus.SndSent -> MR.images.ic_check_filled to metaColor
|
||||
is CIStatus.SndRcvd -> when(itemStatus.msgRcptStatus) {
|
||||
MsgReceiptStatus.Ok -> MR.images.ic_double_check to metaColor
|
||||
MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red
|
||||
}
|
||||
is CIStatus.SndErrorAuth -> MR.images.ic_close to Color.Red
|
||||
is CIStatus.SndError -> MR.images.ic_warning_filled to WarningYellow
|
||||
is CIStatus.RcvNew -> MR.images.ic_circle_filled to primaryColor
|
||||
else -> null
|
||||
}
|
||||
fun statusIcon(
|
||||
primaryColor: Color,
|
||||
metaColor: Color = CurrentColors.value.colors.secondary,
|
||||
paleMetaColor: Color = CurrentColors.value.colors.secondary
|
||||
): Pair<ImageResource, Color>? =
|
||||
itemStatus.statusIcon(primaryColor, metaColor, paleMetaColor)
|
||||
|
||||
companion object {
|
||||
fun getSample(
|
||||
@@ -1714,12 +1709,56 @@ fun localTimestamp(t: Instant): String {
|
||||
@Serializable
|
||||
sealed class CIStatus {
|
||||
@Serializable @SerialName("sndNew") class SndNew: CIStatus()
|
||||
@Serializable @SerialName("sndSent") class SndSent: CIStatus()
|
||||
@Serializable @SerialName("sndRcvd") class SndRcvd(val msgRcptStatus: MsgReceiptStatus): CIStatus()
|
||||
@Serializable @SerialName("sndSent") class SndSent(val sndProgress: SndCIStatusProgress): CIStatus()
|
||||
@Serializable @SerialName("sndRcvd") class SndRcvd(val msgRcptStatus: MsgReceiptStatus, val sndProgress: SndCIStatusProgress): CIStatus()
|
||||
@Serializable @SerialName("sndErrorAuth") class SndErrorAuth: CIStatus()
|
||||
@Serializable @SerialName("sndError") class SndError(val agentError: String): CIStatus()
|
||||
@Serializable @SerialName("rcvNew") class RcvNew: CIStatus()
|
||||
@Serializable @SerialName("rcvRead") class RcvRead: CIStatus()
|
||||
|
||||
fun statusIcon(
|
||||
primaryColor: Color,
|
||||
metaColor: Color = CurrentColors.value.colors.secondary,
|
||||
paleMetaColor: Color = CurrentColors.value.colors.secondary
|
||||
): Pair<ImageResource, Color>? =
|
||||
when (this) {
|
||||
is SndNew -> null
|
||||
is SndSent -> when (this.sndProgress) {
|
||||
SndCIStatusProgress.Complete -> MR.images.ic_check_filled to metaColor
|
||||
SndCIStatusProgress.Partial -> MR.images.ic_check_filled to paleMetaColor
|
||||
}
|
||||
is SndRcvd -> when(this.msgRcptStatus) {
|
||||
MsgReceiptStatus.Ok -> when (this.sndProgress) {
|
||||
SndCIStatusProgress.Complete -> MR.images.ic_double_check to metaColor
|
||||
SndCIStatusProgress.Partial -> MR.images.ic_double_check to paleMetaColor
|
||||
}
|
||||
MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red
|
||||
}
|
||||
is SndErrorAuth -> MR.images.ic_close to Color.Red
|
||||
is SndError -> MR.images.ic_warning_filled to WarningYellow
|
||||
is RcvNew -> MR.images.ic_circle_filled to primaryColor
|
||||
is RcvRead -> null
|
||||
}
|
||||
|
||||
val statusText: String get() = when (this) {
|
||||
is SndNew -> generalGetString(MR.strings.item_status_snd_new_text)
|
||||
is SndSent -> generalGetString(MR.strings.item_status_snd_sent_text)
|
||||
is SndRcvd -> generalGetString(MR.strings.item_status_snd_rcvd_text)
|
||||
is SndErrorAuth -> generalGetString(MR.strings.item_status_snd_error_text)
|
||||
is SndError -> generalGetString(MR.strings.item_status_snd_error_text)
|
||||
is RcvNew -> generalGetString(MR.strings.item_status_rcv_new_text)
|
||||
is RcvRead -> generalGetString(MR.strings.item_status_rcv_read_text)
|
||||
}
|
||||
|
||||
val statusDescription: String get() = when (this) {
|
||||
is SndNew -> generalGetString(MR.strings.item_status_snd_new_desc)
|
||||
is SndSent -> generalGetString(MR.strings.item_status_snd_sent_desc)
|
||||
is SndRcvd -> generalGetString(MR.strings.item_status_snd_rcvd_desc)
|
||||
is SndErrorAuth -> generalGetString(MR.strings.item_status_snd_error_auth_desc)
|
||||
is SndError -> String.format(generalGetString(MR.strings.item_status_snd_error_unexpected_desc), this.agentError)
|
||||
is RcvNew -> generalGetString(MR.strings.item_status_rcv_new_desc)
|
||||
is RcvRead -> generalGetString(MR.strings.item_status_rcv_read_desc)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -1728,6 +1767,12 @@ enum class MsgReceiptStatus {
|
||||
@SerialName("badMsgHash") BadMsgHash;
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class SndCIStatusProgress {
|
||||
@SerialName("partial") Partial,
|
||||
@SerialName("complete") Complete;
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class CIDeleted {
|
||||
@Serializable @SerialName("deleted") class Deleted(val deletedTs: Instant?): CIDeleted()
|
||||
@@ -2488,6 +2533,7 @@ sealed class ChatItemTTL: Comparable<ChatItemTTL?> {
|
||||
@Serializable
|
||||
class ChatItemInfo(
|
||||
val itemVersions: List<ChatItemVersion>,
|
||||
val memberDeliveryStatuses: List<MemberDeliveryStatus>?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -2499,6 +2545,12 @@ data class ChatItemVersion(
|
||||
val createdAt: Instant,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MemberDeliveryStatus(
|
||||
val groupMemberId: Long,
|
||||
val memberDeliveryStatus: CIStatus
|
||||
)
|
||||
|
||||
enum class NotificationPreviewMode {
|
||||
MESSAGE, CONTACT, HIDDEN;
|
||||
|
||||
|
||||
@@ -471,13 +471,19 @@ object ChatController {
|
||||
suspend fun apiSetAllContactReceipts(enable: Boolean) {
|
||||
val r = sendCmd(CC.SetAllContactReceipts(enable))
|
||||
if (r is CR.CmdOk) return
|
||||
throw Exception("failed to enable receipts for all users ${r.responseType} ${r.details}")
|
||||
throw Exception("failed to set receipts for all users ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiSetUserContactReceipts(userId: Long, userMsgReceiptSettings: UserMsgReceiptSettings) {
|
||||
val r = sendCmd(CC.ApiSetUserContactReceipts(userId, userMsgReceiptSettings))
|
||||
if (r is CR.CmdOk) return
|
||||
throw Exception("failed to enable receipts for user contacts ${r.responseType} ${r.details}")
|
||||
throw Exception("failed to set receipts for user contacts ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiSetUserGroupReceipts(userId: Long, userMsgReceiptSettings: UserMsgReceiptSettings) {
|
||||
val r = sendCmd(CC.ApiSetUserGroupReceipts(userId, userMsgReceiptSettings))
|
||||
if (r is CR.CmdOk) return
|
||||
throw Exception("failed to set receipts for user groups ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiHideUser(userId: Long, viewPwd: String): User =
|
||||
@@ -1785,6 +1791,7 @@ sealed class CC {
|
||||
class ApiSetActiveUser(val userId: Long, val viewPwd: String?): CC()
|
||||
class SetAllContactReceipts(val enable: Boolean): CC()
|
||||
class ApiSetUserContactReceipts(val userId: Long, val userMsgReceiptSettings: UserMsgReceiptSettings): CC()
|
||||
class ApiSetUserGroupReceipts(val userId: Long, val userMsgReceiptSettings: UserMsgReceiptSettings): CC()
|
||||
class ApiHideUser(val userId: Long, val viewPwd: String): CC()
|
||||
class ApiUnhideUser(val userId: Long, val viewPwd: String): CC()
|
||||
class ApiMuteUser(val userId: Long): CC()
|
||||
@@ -1884,6 +1891,10 @@ sealed class CC {
|
||||
val mrs = userMsgReceiptSettings
|
||||
"/_set receipts contacts $userId ${onOff(mrs.enable)} clear_overrides=${onOff(mrs.clearOverrides)}"
|
||||
}
|
||||
is ApiSetUserGroupReceipts -> {
|
||||
val mrs = userMsgReceiptSettings
|
||||
"/_set receipts groups $userId ${onOff(mrs.enable)} clear_overrides=${onOff(mrs.clearOverrides)}"
|
||||
}
|
||||
is ApiHideUser -> "/_hide user $userId ${json.encodeToString(viewPwd)}"
|
||||
is ApiUnhideUser -> "/_unhide user $userId ${json.encodeToString(viewPwd)}"
|
||||
is ApiMuteUser -> "/_mute user $userId"
|
||||
@@ -1981,6 +1992,7 @@ sealed class CC {
|
||||
is ApiSetActiveUser -> "apiSetActiveUser"
|
||||
is SetAllContactReceipts -> "setAllContactReceipts"
|
||||
is ApiSetUserContactReceipts -> "apiSetUserContactReceipts"
|
||||
is ApiSetUserGroupReceipts -> "apiSetUserGroupReceipts"
|
||||
is ApiHideUser -> "apiHideUser"
|
||||
is ApiUnhideUser -> "apiUnhideUser"
|
||||
is ApiMuteUser -> "apiMuteUser"
|
||||
|
||||
@@ -11,5 +11,6 @@ expect fun PlatformTextField(
|
||||
textStyle: MutableState<TextStyle>,
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
onMessageChange: (String) -> Unit
|
||||
onMessageChange: (String) -> Unit,
|
||||
onDone: () -> Unit,
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import chat.simplex.common.ui.theme.DEFAULT_MIN_CENTER_MODAL_WIDTH
|
||||
import com.russhwolf.settings.Settings
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
|
||||
@@ -30,4 +31,7 @@ expect fun screenWidth(): Dp
|
||||
|
||||
expect fun desktopExpandWindowToWidth(width: Dp)
|
||||
|
||||
@Composable
|
||||
expect fun allowToShowBackButtonInCenter(): Boolean
|
||||
|
||||
expect fun isRtl(text: CharSequence): Boolean
|
||||
|
||||
@@ -34,7 +34,7 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
|
||||
close()
|
||||
})
|
||||
TerminalLayout(
|
||||
remember { chatModel.terminalItems },
|
||||
chatModel.terminalItems,
|
||||
composeState,
|
||||
sendCommand = { sendCommand(chatModel, composeState) },
|
||||
close
|
||||
@@ -62,7 +62,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
|
||||
|
||||
@Composable
|
||||
fun TerminalLayout(
|
||||
terminalItems: List<TerminalItem>,
|
||||
terminalItems: MutableState<List<TerminalItem>>,
|
||||
composeState: MutableState<ComposeState>,
|
||||
sendCommand: () -> Unit,
|
||||
close: () -> Unit
|
||||
@@ -115,12 +115,12 @@ fun TerminalLayout(
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
@Composable
|
||||
fun TerminalLog(terminalItems: List<TerminalItem>) {
|
||||
fun TerminalLog(terminalItems: MutableState<List<TerminalItem>>) {
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
}
|
||||
val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } }
|
||||
val reversedTerminalItems by remember { derivedStateOf { terminalItems.value.reversed().toList() } }
|
||||
val clipboard = LocalClipboardManager.current
|
||||
LazyColumn(state = listState, reverseLayout = true) {
|
||||
items(reversedTerminalItems) { item ->
|
||||
@@ -152,7 +152,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
|
||||
fun PreviewTerminalLayout() {
|
||||
SimpleXTheme {
|
||||
TerminalLayout(
|
||||
terminalItems = TerminalItem.sampleData,
|
||||
terminalItems = remember { mutableStateOf(TerminalItem.sampleData) },
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) },
|
||||
sendCommand = {},
|
||||
close = {}
|
||||
|
||||
@@ -3,6 +3,7 @@ package chat.simplex.common.views.chat
|
||||
import InfoRow
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -19,28 +20,30 @@ import androidx.compose.ui.text.AnnotatedString
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.chat.item.MarkdownText
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.platform.shareText
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
|
||||
enum class CIInfoTab {
|
||||
History, Quote
|
||||
sealed class CIInfoTab {
|
||||
class Delivery(val memberDeliveryStatuses: List<MemberDeliveryStatus>): CIInfoTab()
|
||||
object History: CIInfoTab()
|
||||
class Quote(val quotedItem: CIQuote): CIInfoTab()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
|
||||
fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
|
||||
val sent = ci.chatDir.sent
|
||||
val appColors = CurrentColors.collectAsState().value.appColors
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val selection = remember { mutableStateOf(CIInfoTab.History) }
|
||||
val selection = remember { mutableStateOf<CIInfoTab>(CIInfoTab.History) }
|
||||
|
||||
@Composable
|
||||
fun TextBubble(text: String, formattedText: List<FormattedText>?, sender: String?, showMenu: MutableState<Boolean>) {
|
||||
@@ -160,10 +163,12 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
|
||||
if (itemDeleted.deletedTs != null) {
|
||||
InfoRow(stringResource(MR.strings.info_row_deleted_at), localTimestamp(itemDeleted.deletedTs))
|
||||
}
|
||||
|
||||
is CIDeleted.Moderated ->
|
||||
if (itemDeleted.deletedTs != null) {
|
||||
InfoRow(stringResource(MR.strings.info_row_moderated_at), localTimestamp(itemDeleted.deletedTs))
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
val deleteAt = ci.meta.itemTimed?.deleteAt
|
||||
@@ -214,55 +219,154 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus) {
|
||||
SectionItemView(
|
||||
padding = PaddingValues(horizontal = 0.dp)
|
||||
) {
|
||||
ProfileImage(size = 36.dp, member.image)
|
||||
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
|
||||
Text(
|
||||
member.chatViewName,
|
||||
modifier = Modifier.weight(10f, fill = true),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
val statusIcon = status.statusIcon(MaterialTheme.colors.primary, CurrentColors.value.colors.secondary)
|
||||
Box(
|
||||
Modifier
|
||||
.size(36.dp)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.clickable {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = status.statusText,
|
||||
text = status.statusDescription
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (statusIcon != null) {
|
||||
val (icon, statusColor) = statusIcon
|
||||
Icon(
|
||||
painterResource(icon),
|
||||
contentDescription = null,
|
||||
tint = statusColor
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_more_horiz),
|
||||
contentDescription = null,
|
||||
tint = CurrentColors.value.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeliveryTab(memberDeliveryStatuses: List<MemberDeliveryStatus>) {
|
||||
Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
|
||||
Details()
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
|
||||
val mss = membersStatuses(chatModel, memberDeliveryStatuses)
|
||||
if (mss.isNotEmpty()) {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(stringResource(MR.strings.delivery), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
mss.forEach { (member, status) ->
|
||||
MemberDeliveryStatusView(member, status)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(stringResource(MR.strings.no_info_on_delivery), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun tabTitle(tab: CIInfoTab): String {
|
||||
return when (tab) {
|
||||
CIInfoTab.History -> stringResource(MR.strings.edit_history)
|
||||
CIInfoTab.Quote -> stringResource(MR.strings.in_reply_to)
|
||||
is CIInfoTab.Delivery -> stringResource(MR.strings.delivery)
|
||||
is CIInfoTab.History -> stringResource(MR.strings.edit_history)
|
||||
is CIInfoTab.Quote -> stringResource(MR.strings.in_reply_to)
|
||||
}
|
||||
}
|
||||
|
||||
fun tabIcon(tab: CIInfoTab): ImageResource {
|
||||
return when (tab) {
|
||||
CIInfoTab.History -> MR.images.ic_history
|
||||
CIInfoTab.Quote -> MR.images.ic_reply
|
||||
is CIInfoTab.Delivery -> MR.images.ic_double_check
|
||||
is CIInfoTab.History -> MR.images.ic_history
|
||||
is CIInfoTab.Quote -> MR.images.ic_reply
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
fun numTabs(): Int {
|
||||
var numTabs = 1
|
||||
if (ciInfo.memberDeliveryStatuses != null) {
|
||||
numTabs += 1
|
||||
}
|
||||
if (ci.quotedItem != null) {
|
||||
numTabs += 1
|
||||
}
|
||||
return numTabs
|
||||
}
|
||||
|
||||
Column {
|
||||
if (numTabs() > 1) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
if (ciInfo.memberDeliveryStatuses != null) {
|
||||
selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses)
|
||||
}
|
||||
}
|
||||
Column(Modifier.weight(1f)) {
|
||||
when (selection.value) {
|
||||
CIInfoTab.History -> {
|
||||
when (val sel = selection.value) {
|
||||
is CIInfoTab.Delivery -> {
|
||||
DeliveryTab(sel.memberDeliveryStatuses)
|
||||
}
|
||||
|
||||
is CIInfoTab.History -> {
|
||||
HistoryTab()
|
||||
}
|
||||
|
||||
CIInfoTab.Quote -> {
|
||||
QuoteTab(ci.quotedItem)
|
||||
is CIInfoTab.Quote -> {
|
||||
QuoteTab(sel.quotedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
val availableTabs = mutableListOf<CIInfoTab>()
|
||||
if (ciInfo.memberDeliveryStatuses != null) {
|
||||
availableTabs.add(CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses))
|
||||
}
|
||||
availableTabs.add(CIInfoTab.History)
|
||||
if (ci.quotedItem != null) {
|
||||
availableTabs.add(CIInfoTab.Quote(ci.quotedItem))
|
||||
}
|
||||
TabRow(
|
||||
selectedTabIndex = selection.value.ordinal,
|
||||
selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class },
|
||||
backgroundColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colors.primary,
|
||||
) {
|
||||
CIInfoTab.values().forEachIndexed { index, it ->
|
||||
availableTabs.forEach { ciInfoTab ->
|
||||
Tab(
|
||||
selected = selection.value.ordinal == index,
|
||||
selected = selection.value::class == ciInfoTab::class,
|
||||
onClick = {
|
||||
selection.value = CIInfoTab.values()[index]
|
||||
selection.value = ciInfoTab
|
||||
},
|
||||
text = { Text(tabTitle(it), fontSize = 13.sp) },
|
||||
text = { Text(tabTitle(ciInfoTab), fontSize = 13.sp) },
|
||||
icon = {
|
||||
Icon(
|
||||
painterResource(tabIcon(it)),
|
||||
tabTitle(it)
|
||||
painterResource(tabIcon(ciInfoTab)),
|
||||
tabTitle(ciInfoTab)
|
||||
)
|
||||
},
|
||||
selectedContentColor = MaterialTheme.colors.primary,
|
||||
@@ -277,10 +381,18 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolean): String {
|
||||
private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List<MemberDeliveryStatus>): List<Pair<GroupMember, CIStatus>> {
|
||||
return memberDeliveryStatuses.mapNotNull { mds ->
|
||||
chatModel.groupMembers.firstOrNull { it.groupMemberId == mds.groupMemberId }?.let { mem ->
|
||||
mem to mds.memberDeliveryStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun itemInfoShareText(chatModel: ChatModel, ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolean): String {
|
||||
val meta = ci.meta
|
||||
val sent = ci.chatDir.sent
|
||||
val shareText = mutableListOf<String>(generalGetString(if (sent) MR.strings.sent_message else MR.strings.received_message), "")
|
||||
val shareText = mutableListOf<String>("# " + generalGetString(if (sent) MR.strings.sent_message else MR.strings.received_message), "")
|
||||
|
||||
shareText.add(String.format(generalGetString(MR.strings.share_text_sent_at), localTimestamp(meta.itemTs)))
|
||||
if (!ci.chatDir.sent) {
|
||||
@@ -291,10 +403,12 @@ fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolea
|
||||
if (itemDeleted.deletedTs != null) {
|
||||
shareText.add(String.format(generalGetString(MR.strings.share_text_deleted_at), localTimestamp(itemDeleted.deletedTs)))
|
||||
}
|
||||
|
||||
is CIDeleted.Moderated ->
|
||||
if (itemDeleted.deletedTs != null) {
|
||||
shareText.add(String.format(generalGetString(MR.strings.share_text_moderated_at), localTimestamp(itemDeleted.deletedTs)))
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
val deleteAt = ci.meta.itemTimed?.deleteAt
|
||||
@@ -308,7 +422,7 @@ fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolea
|
||||
val qi = ci.quotedItem
|
||||
if (qi != null) {
|
||||
shareText.add("")
|
||||
shareText.add(generalGetString(MR.strings.in_reply_to))
|
||||
shareText.add("## " + generalGetString(MR.strings.in_reply_to))
|
||||
shareText.add("")
|
||||
val ts = localTimestamp(qi.sentAt)
|
||||
val sender = qi.sender(null)
|
||||
@@ -320,10 +434,26 @@ fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolea
|
||||
val t = qi.text
|
||||
shareText.add(if (t != "") t else generalGetString(MR.strings.item_info_no_text))
|
||||
}
|
||||
val mdss = chatItemInfo.memberDeliveryStatuses
|
||||
if (mdss != null) {
|
||||
val mss = membersStatuses(chatModel, mdss)
|
||||
if (mss.isNotEmpty()) {
|
||||
shareText.add("")
|
||||
shareText.add("## " + generalGetString(MR.strings.delivery))
|
||||
shareText.add("")
|
||||
mss.forEach { (member, status) ->
|
||||
shareText.add(String.format(
|
||||
generalGetString(MR.strings.recipient_colon_delivery_status),
|
||||
member.chatViewName,
|
||||
status.statusDescription
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
val versions = chatItemInfo.itemVersions
|
||||
if (versions.isNotEmpty()) {
|
||||
shareText.add("")
|
||||
shareText.add(generalGetString(MR.strings.edit_history))
|
||||
shareText.add("## " + generalGetString(MR.strings.edit_history))
|
||||
versions.forEachIndexed { index, itemVersion ->
|
||||
val ts = localTimestamp(itemVersion.itemVersionTs)
|
||||
shareText.add("")
|
||||
|
||||
@@ -321,11 +321,14 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
withApi {
|
||||
val ciInfo = chatModel.controller.apiGetChatItemInfo(cInfo.chatType, cInfo.apiId, cItem.id)
|
||||
if (ciInfo != null) {
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
|
||||
}
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModal(endButtons = { ShareButton {
|
||||
clipboard.shareText(itemInfoShareText(cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
|
||||
clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
|
||||
} }) {
|
||||
ChatItemInfoView(cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
|
||||
ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -471,7 +474,7 @@ fun ChatInfoToolbar(
|
||||
showSearch = false
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
if (allowToShowBackButtonInCenter()) {
|
||||
BackHandler(onBack = onBackClicked)
|
||||
}
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
@@ -531,7 +534,7 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
|
||||
DefaultTopAppBar(
|
||||
navigationButton = { if (appPlatform.isAndroid || showSearch) { NavigationButtonBack(onBackClicked) } },
|
||||
navigationButton = { if (allowToShowBackButtonInCenter() || showSearch) { Box(Modifier.offset(y = 4.dp)) { NavigationButtonBack(onBackClicked) } } },
|
||||
title = { ChatInfoToolbarTitle(chat.chatInfo) },
|
||||
onTitleClick = info,
|
||||
showSearch = showSearch,
|
||||
|
||||
@@ -67,7 +67,9 @@ fun SendMsgView(
|
||||
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
|
||||
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
|
||||
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
|
||||
PlatformTextField(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange)
|
||||
PlatformTextField(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange) {
|
||||
sendMessage(null)
|
||||
}
|
||||
// Disable clicks on text field
|
||||
if (cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) {
|
||||
Box(
|
||||
|
||||
@@ -26,26 +26,37 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chatlist.cantInviteIncognitoAlert
|
||||
import chat.simplex.common.views.chatlist.setGroupMembers
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.common.model.GroupInfo
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.ClearChatButton
|
||||
import chat.simplex.common.views.chat.clearChatDialog
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
|
||||
|
||||
@Composable
|
||||
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit, close: () -> Unit) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val currentUser = chatModel.currentUser.value
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
if (chat != null && chat.chatInfo is ChatInfo.Group) {
|
||||
if (chat != null && chat.chatInfo is ChatInfo.Group && currentUser != null) {
|
||||
val groupInfo = chat.chatInfo.groupInfo
|
||||
val sendReceipts = remember { mutableStateOf(SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, currentUser.sendRcptsSmallGroups)) }
|
||||
GroupChatInfoLayout(
|
||||
chat,
|
||||
groupInfo,
|
||||
currentUser,
|
||||
sendReceipts = sendReceipts,
|
||||
setSendReceipts = { sendRcpts ->
|
||||
withApi {
|
||||
val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool)
|
||||
updateChatSettings(chat, chatSettings, chatModel)
|
||||
sendReceipts.value = sendRcpts
|
||||
}
|
||||
},
|
||||
members = chatModel.groupMembers
|
||||
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
|
||||
.sortedBy { it.displayName.lowercase() },
|
||||
@@ -150,6 +161,9 @@ fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> U
|
||||
fun GroupChatInfoLayout(
|
||||
chat: Chat,
|
||||
groupInfo: GroupInfo,
|
||||
currentUser: User,
|
||||
sendReceipts: State<SendReceipts>,
|
||||
setSendReceipts: (SendReceipts) -> Unit,
|
||||
members: List<GroupMember>,
|
||||
developerTools: Boolean,
|
||||
groupLink: String?,
|
||||
@@ -184,6 +198,11 @@ fun GroupChatInfoLayout(
|
||||
AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage)
|
||||
}
|
||||
GroupPreferencesButton(openPreferences)
|
||||
if (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
|
||||
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
|
||||
} else {
|
||||
SendReceiptsOptionDisabled()
|
||||
}
|
||||
}
|
||||
SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs))
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
@@ -269,6 +288,37 @@ private fun GroupPreferencesButton(onClick: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendReceiptsOption(currentUser: User, state: State<SendReceipts>, onSelected: (SendReceipts) -> Unit) {
|
||||
val values = remember {
|
||||
mutableListOf(SendReceipts.Yes, SendReceipts.No, SendReceipts.UserDefault(currentUser.sendRcptsSmallGroups)).map { it to it.text }
|
||||
}
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.send_receipts),
|
||||
values,
|
||||
state,
|
||||
icon = painterResource(MR.images.ic_double_check),
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SendReceiptsOptionDisabled() {
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_double_check),
|
||||
text = generalGetString(MR.strings.send_receipts),
|
||||
click = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.send_receipts_disabled_alert_title),
|
||||
text = String.format(generalGetString(MR.strings.send_receipts_disabled_alert_msg), SMALL_GROUPS_RCPS_MEM_LIMIT)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(generalGetString(MR.strings.send_receipts_disabled), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
@@ -429,6 +479,9 @@ fun PreviewGroupChatInfoLayout() {
|
||||
chatItems = arrayListOf()
|
||||
),
|
||||
groupInfo = GroupInfo.sampleData,
|
||||
User.sampleData,
|
||||
sendReceipts = remember { mutableStateOf(SendReceipts.Yes) },
|
||||
setSendReceipts = {},
|
||||
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
|
||||
developerTools = false,
|
||||
groupLink = null,
|
||||
|
||||
@@ -14,11 +14,27 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.isInDarkTheme
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = MaterialTheme.colors.secondary) {
|
||||
fun CIMetaView(
|
||||
chatItem: ChatItem,
|
||||
timedMessagesTTL: Int?,
|
||||
metaColor: Color = MaterialTheme.colors.secondary,
|
||||
paleMetaColor: Color = if (isInDarkTheme()) {
|
||||
metaColor.copy(
|
||||
red = metaColor.red * 0.67F,
|
||||
green = metaColor.green * 0.67F,
|
||||
blue = metaColor.red * 0.67F)
|
||||
} else {
|
||||
metaColor.copy(
|
||||
red = minOf(metaColor.red * 1.33F, 1F),
|
||||
green = minOf(metaColor.green * 1.33F, 1F),
|
||||
blue = minOf(metaColor.red * 1.33F, 1F))
|
||||
}
|
||||
) {
|
||||
Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (chatItem.isDeletedContent) {
|
||||
Text(
|
||||
@@ -28,14 +44,14 @@ fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = Ma
|
||||
modifier = Modifier.padding(start = 3.dp)
|
||||
)
|
||||
} else {
|
||||
CIMetaText(chatItem.meta, timedMessagesTTL, metaColor)
|
||||
CIMetaText(chatItem.meta, timedMessagesTTL, metaColor, paleMetaColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
// changing this function requires updating reserveSpaceForMeta
|
||||
private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
|
||||
private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color, paleColor: Color) {
|
||||
if (meta.itemEdited) {
|
||||
StatusIconText(painterResource(MR.images.ic_edit), color)
|
||||
Spacer(Modifier.width(3.dp))
|
||||
@@ -48,7 +64,7 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color)
|
||||
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor)
|
||||
if (statusIcon != null) {
|
||||
val (icon, statusColor) = statusIcon
|
||||
if (meta.itemStatus is CIStatus.SndSent || meta.itemStatus is CIStatus.SndRcvd) {
|
||||
@@ -138,7 +154,7 @@ fun PreviewCIMetaViewSendNoAuth() {
|
||||
fun PreviewCIMetaViewSendSent() {
|
||||
CIMetaView(
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent()
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent(SndCIStatusProgress.Complete)
|
||||
),
|
||||
null
|
||||
)
|
||||
@@ -176,7 +192,7 @@ fun PreviewCIMetaViewEditedSent() {
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
|
||||
itemEdited = true,
|
||||
status= CIStatus.SndSent()
|
||||
status= CIStatus.SndSent(SndCIStatusProgress.Complete)
|
||||
),
|
||||
null
|
||||
)
|
||||
|
||||
@@ -19,8 +19,7 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.Log
|
||||
import chat.simplex.common.platform.TAG
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.views.helpers.DisposableEffectOnGone
|
||||
import chat.simplex.common.views.helpers.detectGesture
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
val reserveTimestampStyle = SpanStyle(color = Color.Transparent)
|
||||
@@ -149,7 +148,7 @@ fun MarkdownText (
|
||||
} else {
|
||||
ft.format.style
|
||||
}
|
||||
withAnnotation(tag = "URL", annotation = link) {
|
||||
withAnnotation(tag = if (ft.format is Format.SimplexLink && linkMode != SimplexLinkMode.BROWSER) "SIMPLEX_URL" else "URL", annotation = link) {
|
||||
withStyle(ftStyle) { append(ft.viewText(linkMode)) }
|
||||
}
|
||||
} else {
|
||||
@@ -170,6 +169,8 @@ fun MarkdownText (
|
||||
onLongClick = { offset ->
|
||||
annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset)
|
||||
.firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) }
|
||||
annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset)
|
||||
.firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) }
|
||||
},
|
||||
onClick = { offset ->
|
||||
annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset)
|
||||
@@ -182,9 +183,14 @@ fun MarkdownText (
|
||||
Log.e(TAG, "Open url: ${e.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset)
|
||||
.firstOrNull()?.let { annotation ->
|
||||
uriHandler.openVerifiedSimplexUri(annotation.item)
|
||||
}
|
||||
},
|
||||
shouldConsumeEvent = { offset ->
|
||||
annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset).any()
|
||||
annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset).any()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -28,7 +28,7 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(stringResource(MR.strings.thank_you_for_installing_simplex), lineHeight = 22.sp)
|
||||
ReadableTextWithLink(MR.strings.you_can_connect_to_simplex_chat_founder, simplexTeamUri)
|
||||
ReadableTextWithLink(MR.strings.you_can_connect_to_simplex_chat_founder, simplexTeamUri, simplexLink = true)
|
||||
Column(
|
||||
Modifier.padding(top = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chat.group.deleteGroupDialog
|
||||
@@ -24,8 +25,6 @@ import chat.simplex.common.views.chat.item.InvalidJSONView
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.ContactConnectionInfoView
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.platform.ntfManager
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.datetime.Clock
|
||||
@@ -42,6 +41,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
showMenu.value = false
|
||||
delay(500L)
|
||||
}
|
||||
val showClose = allowToShowBackButtonInCenter()
|
||||
when (chat.chatInfo) {
|
||||
is ChatInfo.Direct -> {
|
||||
val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact)
|
||||
@@ -75,7 +75,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
click = {
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close ->
|
||||
ModalManager.center.showModalCloseable(true, showClose = showClose) { close ->
|
||||
ContactConnectionInfoView(chatModel, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close)
|
||||
}
|
||||
},
|
||||
@@ -341,13 +341,14 @@ fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatMo
|
||||
|
||||
@Composable
|
||||
fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
val showClose = allowToShowBackButtonInCenter()
|
||||
ItemAction(
|
||||
stringResource(MR.strings.set_contact_name),
|
||||
painterResource(MR.images.ic_edit),
|
||||
onClick = {
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close ->
|
||||
ModalManager.center.showModalCloseable(true, showClose = showClose) { close ->
|
||||
ContactConnectionInfoView(chatModel, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, true, close)
|
||||
}
|
||||
showMenu.value = false
|
||||
|
||||
@@ -58,7 +58,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
connectIfOpenedViaUri(url, chatModel)
|
||||
}
|
||||
}
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
val endPadding = if (appPlatform.isDesktop && !allowToShowBackButtonInCenter()) 56.dp else 0.dp
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val scope = rememberCoroutineScope()
|
||||
val (userPickerState, scaffoldState, switchingUsers ) = settingsState
|
||||
@@ -130,7 +130,7 @@ private fun OnboardingButtons(openNewChatSheet: () -> Unit) {
|
||||
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
ConnectButton(generalGetString(MR.strings.chat_with_developers)) {
|
||||
uriHandler.openUriCatching(simplexTeamUri)
|
||||
uriHandler.openVerifiedSimplexUri(simplexTeamUri)
|
||||
}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
ConnectButton(generalGetString(MR.strings.tap_to_start_new_chat), openNewChatSheet)
|
||||
|
||||
@@ -18,8 +18,7 @@ import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.Chat
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.BackHandler
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@@ -27,7 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) {
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val (userPickerState, scaffoldState, switchingUsers) = settingsState
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
val endPadding = if (appPlatform.isDesktop && !allowToShowBackButtonInCenter()) 56.dp else 0.dp
|
||||
Scaffold(
|
||||
Modifier.padding(end = endPadding),
|
||||
scaffoldState = scaffoldState,
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.ThemeOverrides
|
||||
import chat.simplex.common.views.chatlist.connectIfOpenedViaUri
|
||||
import chat.simplex.res.MR
|
||||
import com.charleskorn.kaml.decodeFromStream
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
@@ -281,6 +282,13 @@ inline fun <reified T> serializableSaver(): Saver<T, *> = Saver(
|
||||
restore = { json.decodeFromString(it) }
|
||||
)
|
||||
|
||||
fun UriHandler.openVerifiedSimplexUri(uri: String) {
|
||||
val URI = try { URI.create(uri) } catch (e: Exception) { null }
|
||||
if (URI != null) {
|
||||
connectIfOpenedViaUri(URI, ChatModel)
|
||||
}
|
||||
}
|
||||
|
||||
fun UriHandler.openUriCatching(uri: String) {
|
||||
try {
|
||||
openUri(uri)
|
||||
|
||||
@@ -96,7 +96,7 @@ private fun NewChatSheetLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
val endPadding = if (appPlatform.isDesktop && !allowToShowBackButtonInCenter()) 56.dp else 0.dp
|
||||
val maxWidth = with(LocalDensity.current) { screenWidth() * density }
|
||||
Column(
|
||||
Modifier
|
||||
|
||||
@@ -55,7 +55,7 @@ fun ReadableText(stringResId: StringResource, textAlign: TextAlign = TextAlign.S
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReadableTextWithLink(stringResId: StringResource, link: String, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp)) {
|
||||
fun ReadableTextWithLink(stringResId: StringResource, link: String, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp), simplexLink: Boolean = false) {
|
||||
val annotated = annotatedStringResource(stringResId)
|
||||
val primary = MaterialTheme.colors.primary
|
||||
// This replaces links in text highlighted with specific color, e.g. SimplexBlue
|
||||
@@ -71,7 +71,7 @@ fun ReadableTextWithLink(stringResId: StringResource, link: String, textAlign: T
|
||||
newStyles
|
||||
}
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Text(AnnotatedString(annotated.text, newStyles), modifier = Modifier.padding(padding).clickable { uriHandler.openUriCatching(link) }, textAlign = textAlign, lineHeight = 22.sp)
|
||||
Text(AnnotatedString(annotated.text, newStyles), modifier = Modifier.padding(padding).clickable { if (simplexLink) uriHandler.openVerifiedSimplexUri(link) else uriHandler.openUriCatching(link) }, textAlign = textAlign, lineHeight = 22.sp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -101,6 +101,29 @@ fun PrivacySettingsView(
|
||||
}
|
||||
}
|
||||
|
||||
fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) {
|
||||
withApi {
|
||||
val mrs = UserMsgReceiptSettings(enable, clearOverrides)
|
||||
chatModel.controller.apiSetUserGroupReceipts(currentUser.userId, mrs)
|
||||
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
|
||||
chatModel.currentUser.value = currentUser.copy(sendRcptsSmallGroups = enable)
|
||||
if (clearOverrides) {
|
||||
// For loop here is to prevent ConcurrentModificationException that happens with forEach
|
||||
for (i in 0 until chatModel.chats.size) {
|
||||
val chat = chatModel.chats[i]
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
var groupInfo = chat.chatInfo.groupInfo
|
||||
val sendRcpts = groupInfo.chatSettings.sendRcpts
|
||||
if (sendRcpts != null && sendRcpts != enable) {
|
||||
groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null))
|
||||
chatModel.updateGroup(groupInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DeliveryReceiptsSection(
|
||||
currentUser = currentUser,
|
||||
setOrAskSendReceiptsContacts = { enable ->
|
||||
@@ -117,6 +140,21 @@ fun PrivacySettingsView(
|
||||
} else {
|
||||
showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts)
|
||||
}
|
||||
},
|
||||
setOrAskSendReceiptsGroups = { enable ->
|
||||
val groupReceiptsOverrides = chatModel.chats.fold(0) { count, chat ->
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts
|
||||
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
|
||||
} else {
|
||||
count
|
||||
}
|
||||
}
|
||||
if (groupReceiptsOverrides == 0) {
|
||||
setSendReceiptsGroups(enable, clearOverrides = false)
|
||||
} else {
|
||||
showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -155,6 +193,7 @@ expect fun PrivacyDeviceSection(
|
||||
private fun DeliveryReceiptsSection(
|
||||
currentUser: User,
|
||||
setOrAskSendReceiptsContacts: (Boolean) -> Unit,
|
||||
setOrAskSendReceiptsGroups: (Boolean) -> Unit,
|
||||
) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_delivery_receipts)) {
|
||||
SettingsActionItemWithContent(painterResource(MR.images.ic_person), stringResource(MR.strings.receipts_section_contacts)) {
|
||||
@@ -165,6 +204,14 @@ private fun DeliveryReceiptsSection(
|
||||
}
|
||||
)
|
||||
}
|
||||
SettingsActionItemWithContent(painterResource(MR.images.ic_group), stringResource(MR.strings.receipts_section_groups)) {
|
||||
DefaultSwitch(
|
||||
checked = currentUser.sendRcptsSmallGroups ?: false,
|
||||
onCheckedChange = { enable ->
|
||||
setOrAskSendReceiptsGroups(enable)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(
|
||||
remember(currentUser.displayName) {
|
||||
@@ -215,6 +262,41 @@ private fun showUserContactsReceiptsAlert(
|
||||
)
|
||||
}
|
||||
|
||||
private fun showUserGroupsReceiptsAlert(
|
||||
enable: Boolean,
|
||||
groupReceiptsOverrides: Int,
|
||||
setSendReceiptsGroups: (Boolean, Boolean) -> Unit
|
||||
) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(if (enable) MR.strings.receipts_groups_title_enable else MR.strings.receipts_groups_title_disable),
|
||||
text = AnnotatedString(String.format(generalGetString(if (enable) MR.strings.receipts_groups_override_disabled else MR.strings.receipts_groups_override_enabled), groupReceiptsOverrides)),
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
setSendReceiptsGroups(enable, false)
|
||||
}) {
|
||||
val t = stringResource(if (enable) MR.strings.receipts_groups_enable_keep_overrides else MR.strings.receipts_groups_disable_keep_overrides)
|
||||
Text(t, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
setSendReceiptsGroups(enable, true)
|
||||
}
|
||||
) {
|
||||
val t = stringResource(if (enable) MR.strings.receipts_groups_enable_for_all else MR.strings.receipts_groups_disable_for_all)
|
||||
Text(t, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private val laDelays = listOf(10, 30, 60, 180, 0)
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -73,7 +73,7 @@ private fun SetDeliveryReceiptsLayout(
|
||||
skip: () -> Unit,
|
||||
userCount: Int,
|
||||
) {
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
val endPadding = if (appPlatform.isDesktop && !allowToShowBackButtonInCenter()) 56.dp else 0.dp
|
||||
Column(
|
||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING, end = endPadding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
||||
@@ -175,7 +175,7 @@ fun SettingsLayout(
|
||||
SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName) }, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openUriCatching(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_mail), stringResource(MR.strings.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary, extraPadding = true)
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
@@ -222,6 +222,8 @@
|
||||
<string name="edit_history">History</string>
|
||||
<string name="no_history">No history</string>
|
||||
<string name="in_reply_to">In reply to</string>
|
||||
<string name="delivery">Delivery</string>
|
||||
<string name="no_info_on_delivery">No info on delivery</string>
|
||||
<string name="delete_verb">Delete</string>
|
||||
<string name="reveal_verb">Reveal</string>
|
||||
<string name="hide_verb">Hide</string>
|
||||
@@ -869,7 +871,7 @@
|
||||
<string name="if_you_enter_passcode_data_removed">If you enter this passcode when opening the app, all app data will be irreversibly removed!</string>
|
||||
<string name="set_passcode">Set passcode</string>
|
||||
<string name="receipts_section_description">These settings are for your current profile</string>
|
||||
<string name="receipts_section_description_1">They can be overridden in contact settings</string>
|
||||
<string name="receipts_section_description_1">They can be overridden in contact and group settings.</string>
|
||||
<string name="receipts_section_contacts">Contacts</string>
|
||||
<string name="receipts_contacts_title_enable">Enable receipts?</string>
|
||||
<string name="receipts_contacts_title_disable">Disable receipts?</string>
|
||||
@@ -879,6 +881,15 @@
|
||||
<string name="receipts_contacts_disable_keep_overrides">Disable (keep overrides)</string>
|
||||
<string name="receipts_contacts_enable_for_all">Enable for all</string>
|
||||
<string name="receipts_contacts_disable_for_all">Disable for all</string>
|
||||
<string name="receipts_section_groups">Small groups (max 20)</string>
|
||||
<string name="receipts_groups_title_enable">Enable receipts for groups?</string>
|
||||
<string name="receipts_groups_title_disable">Disable receipts for groups?</string>
|
||||
<string name="receipts_groups_override_enabled">Sending receipts is enabled for %d groups</string>
|
||||
<string name="receipts_groups_override_disabled">Sending receipts is disabled for %d groups</string>
|
||||
<string name="receipts_groups_enable_keep_overrides">Enable (keep group overrides)</string>
|
||||
<string name="receipts_groups_disable_keep_overrides">Disable (keep group overrides)</string>
|
||||
<string name="receipts_groups_enable_for_all">Enable for all groups</string>
|
||||
<string name="receipts_groups_disable_for_all">Disable for all groups</string>
|
||||
|
||||
<!-- Settings sections -->
|
||||
<string name="settings_section_title_you">YOU</string>
|
||||
@@ -1161,6 +1172,9 @@
|
||||
<string name="share_address">Share address</string>
|
||||
<string name="you_can_share_this_address_with_your_contacts">You can share this address with your contacts to let them connect with %s.</string>
|
||||
<string name="send_receipts">Send receipts</string>
|
||||
<string name="send_receipts_disabled">disabled</string>
|
||||
<string name="send_receipts_disabled_alert_title">Receipts are disabled</string>
|
||||
<string name="send_receipts_disabled_alert_msg">This group has over %1$d members, delivery receipts are not sent.</string>
|
||||
|
||||
<!-- Chat / Chat item info -->
|
||||
<string name="section_title_for_console">FOR CONSOLE</string>
|
||||
@@ -1183,6 +1197,7 @@
|
||||
<string name="sender_at_ts">%s at %s</string>
|
||||
<string name="current_version_timestamp">%s (current)</string>
|
||||
<string name="item_info_no_text">no text</string>
|
||||
<string name="recipient_colon_delivery_status">%s: %s</string>
|
||||
|
||||
<!-- GroupMemberInfoView.kt -->
|
||||
<string name="button_remove_member">Remove member</string>
|
||||
@@ -1527,6 +1542,22 @@
|
||||
<string name="you_can_enable_delivery_receipts_later_alert">You can enable them later via app Privacy & Security settings.</string>
|
||||
<string name="error_enabling_delivery_receipts">Error enabling delivery receipts!</string>
|
||||
|
||||
<!-- CIStatus texts -->
|
||||
<string name="item_status_snd_new_text">Sending message</string>
|
||||
<string name="item_status_snd_sent_text">Message sent</string>
|
||||
<string name="item_status_snd_rcvd_text">Sent message received</string>
|
||||
<string name="item_status_snd_error_text">Error sending message</string>
|
||||
<string name="item_status_rcv_new_text">Message received</string>
|
||||
<string name="item_status_rcv_read_text">Message read</string>
|
||||
|
||||
<string name="item_status_snd_new_desc">Sending message is in progress or pending.</string>
|
||||
<string name="item_status_snd_sent_desc">Message has been sent to the recipient\'s relay.</string>
|
||||
<string name="item_status_snd_rcvd_desc">Message has been received by the recipient.</string>
|
||||
<string name="item_status_snd_error_auth_desc">Message delivery error. Most likely this recipient has deleted the connection with you.</string>
|
||||
<string name="item_status_snd_error_unexpected_desc">Unexpected message delivery error: %1$s</string>
|
||||
<string name="item_status_rcv_new_desc">New message from this sender.</string>
|
||||
<string name="item_status_rcv_read_desc">You\'ve read this received message.</string>
|
||||
|
||||
<!-- Under development -->
|
||||
<string name="in_developing_title">Coming soon!</string>
|
||||
<string name="in_developing_desc">This feature is not yet supported. Try the next release.</string>
|
||||
|
||||
@@ -14,10 +14,13 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.views.chat.*
|
||||
@@ -33,7 +36,8 @@ actual fun PlatformTextField(
|
||||
textStyle: MutableState<TextStyle>,
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
onMessageChange: (String) -> Unit
|
||||
onMessageChange: (String) -> Unit,
|
||||
onDone: () -> Unit,
|
||||
) {
|
||||
val cs = composeState.value
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@@ -47,11 +51,14 @@ actual fun PlatformTextField(
|
||||
keyboard?.show()
|
||||
}
|
||||
val isRtl = remember(cs.message) { isRtl(cs.message.subSequence(0, min(50, cs.message.length))) }
|
||||
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message)) }
|
||||
val textFieldValue = textFieldValueState.copy(text = cs.message)
|
||||
BasicTextField(
|
||||
value = cs.message,
|
||||
value = textFieldValue,
|
||||
onValueChange = {
|
||||
if (!composeState.value.inProgress && !(composeState.value.preview is ComposePreview.VoicePreview && it != "")) {
|
||||
onMessageChange(it)
|
||||
if (!composeState.value.inProgress && !(composeState.value.preview is ComposePreview.VoicePreview && it.text != "")) {
|
||||
textFieldValueState = it
|
||||
onMessageChange(it.text)
|
||||
}
|
||||
},
|
||||
textStyle = textStyle.value,
|
||||
@@ -60,7 +67,24 @@ actual fun PlatformTextField(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
autoCorrect = true
|
||||
),
|
||||
modifier = Modifier.padding(vertical = 4.dp).focusRequester(focusRequester),
|
||||
modifier = Modifier
|
||||
.padding(vertical = 4.dp)
|
||||
.focusRequester(focusRequester)
|
||||
.onPreviewKeyEvent {
|
||||
if (it.key == Key.Enter && it.type == KeyEventType.KeyDown) {
|
||||
if (it.isShiftPressed) {
|
||||
val newText = textFieldValue.text + "\n"
|
||||
textFieldValueState = textFieldValue.copy(
|
||||
text = newText,
|
||||
selection = TextRange(newText.length, newText.length)
|
||||
)
|
||||
onMessageChange(newText)
|
||||
} else if (cs.message.isNotEmpty()) {
|
||||
onDone()
|
||||
}
|
||||
true
|
||||
} else false
|
||||
},
|
||||
cursorBrush = SolidColor(MaterialTheme.colors.secondary),
|
||||
decorationBox = { innerTextField ->
|
||||
Surface(
|
||||
|
||||
@@ -6,6 +6,8 @@ import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.simplexWindowState
|
||||
import chat.simplex.common.ui.theme.DEFAULT_MIN_CENTER_MODAL_WIDTH
|
||||
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
|
||||
import com.russhwolf.settings.*
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import dev.icerock.moko.resources.desc.desc
|
||||
@@ -54,6 +56,9 @@ actual fun desktopExpandWindowToWidth(width: Dp) {
|
||||
simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = width)
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun allowToShowBackButtonInCenter(): Boolean = simplexWindowState.windowState.size.width < DEFAULT_START_MODAL_WIDTH + DEFAULT_MIN_CENTER_MODAL_WIDTH
|
||||
|
||||
actual fun isRtl(text: CharSequence): Boolean {
|
||||
if (text.isEmpty()) return false
|
||||
return text.any { char ->
|
||||
|
||||
@@ -30,8 +30,16 @@ kotlin {
|
||||
compose {
|
||||
desktop {
|
||||
application {
|
||||
// For debugging via VisualVM
|
||||
/*jvmArgs += listOf(
|
||||
"-Dcom.sun.management.jmxremote.port=8080",
|
||||
"-Dcom.sun.management.jmxremote.ssl=false",
|
||||
"-Dcom.sun.management.jmxremote.authenticate=false"
|
||||
)*/
|
||||
mainClass = "chat.simplex.desktop.MainKt"
|
||||
nativeDistributions {
|
||||
// For debugging via VisualVM
|
||||
//modules("jdk.zipfs", "jdk.management.agent")
|
||||
modules("jdk.zipfs")
|
||||
//includeAllModules = true
|
||||
outputBaseDir.set(project.file("../release"))
|
||||
@@ -44,15 +52,15 @@ compose {
|
||||
appCategory = "Messenger"
|
||||
}
|
||||
windows {
|
||||
// LALAL
|
||||
packageName = "SimpleX"
|
||||
iconFile.set(project.file("src/jvmMain/resources/distribute/simplex.ico"))
|
||||
console = true
|
||||
perUserInstall = true
|
||||
dirChooser = true
|
||||
}
|
||||
macOS {
|
||||
// LALAL
|
||||
//iconFile.set(project.file("../desktop/src/jvmMain/resources/distribute/simplex.icns"))
|
||||
packageName = "SimpleX"
|
||||
iconFile.set(project.file("src/jvmMain/resources/distribute/simplex.icns"))
|
||||
appCategory = "public.app-category.social-networking"
|
||||
bundleID = "chat.simplex.app"
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
@@ -25,8 +25,8 @@ android.nonTransitiveRClass=true
|
||||
android.enableJetifier=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
|
||||
android.version_name=5.2
|
||||
android.version_code=134
|
||||
android.version_name=5.3-beta.0
|
||||
android.version_code=136
|
||||
|
||||
desktop.version_name=1.0
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: simplex-chat
|
||||
version: 5.2.0.4
|
||||
version: 5.3.0.0
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
sudo apt install git openjdk-17-jdk make cmake gcc g++ libssl-dev zlib1g zlib1g-dev pkg-config build-essential curl libffi-dev libgmp-dev libgmp10 libncurses-dev libncurses5 libtinfo5
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
|
||||
ghcup install ghc 8.10.7
|
||||
ghcup set ghc 8.10.7
|
||||
cabal update
|
||||
git clone https://github.com/simplex-chat/simplex-chat -b master simplex
|
||||
cd simplex
|
||||
echo "ignore-project: False" >> cabal.project.local
|
||||
echo "package direct-sqlcipher" >> cabal.project.local
|
||||
echo " flags: +openssl" >> cabal.project.local
|
||||
scripts/desktop/build-lib-linux.sh
|
||||
|
||||
cd apps/multiplatform
|
||||
sed -i 's|":android", ||' settings.gradle.kts
|
||||
|
||||
./gradlew packageDeb
|
||||
#sudo dpkg -i ./release/main/deb/simpl*.deb
|
||||
#sudo ln -s /opt/simplex/bin/simplex /usr/bin/simplex
|
||||
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 5.2.0.4
|
||||
version: 5.3.0.0
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
|
||||
Reference in New Issue
Block a user