iOS: show message sent/unread status (#293)

* light github image for dark mode

* show message received status, remove chevrons in chat list

* show unread message status

* add message send error mark

* refactor alerts to use AlertManager

* show alert message on tapping undelivered message, simplify text-only alerts
This commit is contained in:
Evgeny Poberezkin
2022-02-12 15:59:43 +00:00
committed by GitHub
parent af5abae558
commit 9d9bb68d50
34 changed files with 635 additions and 275 deletions

View File

@@ -1,17 +1,17 @@
{
"images" : [
{
"filename" : "github32px.png",
"filename" : "github_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "github64px.png",
"filename" : "github_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "github64px-1.png",
"filename" : "github_3x.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "github_light_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "github_light_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "github_light_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -9,6 +9,7 @@ import SwiftUI
struct ContentView: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var alertManager = AlertManager.shared
@State private var showNotificationAlert = false
var body: some View {
@@ -23,27 +24,49 @@ struct ContentView: View {
}
ChatReceiver.shared.start()
NtfManager.shared.requestAuthorization(onDeny: {
showNotificationAlert = true
alertManager.showAlert(notificationAlert())
})
}
.alert(isPresented : $showNotificationAlert){
Alert(
title: Text("Notification are disabled!"),
message: Text("Please open settings to enable"),
primaryButton: .default(Text("Open Settings")) {
DispatchQueue.main.async {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
}
},
secondaryButton: .cancel()
)
}
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
} else {
WelcomeView()
}
}
func notificationAlert() -> Alert {
Alert(
title: Text("Notification are disabled!"),
message: Text("Please open settings to enable"),
primaryButton: .default(Text("Open Settings")) {
DispatchQueue.main.async {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
}
},
secondaryButton: .cancel()
)
}
}
final class AlertManager: ObservableObject {
static let shared = AlertManager()
@Published var presentAlert = false
@Published var alertView: Alert?
func showAlert(_ alert: Alert) {
DispatchQueue.main.async {
self.alertView = alert
self.presentAlert = true
}
}
func showAlertMsg(title: String, message: String? = nil) {
if let message = message {
showAlert(Alert(title: Text(title), message: Text(message)))
} else {
showAlert(Alert(title: Text(title)))
}
}
}
//struct ContentView_Previews: PreviewProvider {
// static var previews: some View {

View File

@@ -17,11 +17,11 @@ final class ChatModel: ObservableObject {
// current chat
@Published var chatId: String?
@Published var chatItems: [ChatItem] = []
@Published var chatToTop: String?
// items in the terminal view
@Published var terminalItems: [TerminalItem] = []
@Published var userAddress: String?
@Published var appOpenUrl: URL?
@Published var connectViaUrl = false
static let shared = ChatModel()
func hasChat(_ id: String) -> Bool {
@@ -43,8 +43,8 @@ final class ChatModel: ObservableObject {
}
func updateChatInfo(_ cInfo: ChatInfo) {
if let ix = getChatIndex(cInfo.id) {
chats[ix].chatInfo = cInfo
if let i = getChatIndex(cInfo.id) {
chats[i].chatInfo = cInfo
}
}
@@ -64,8 +64,8 @@ final class ChatModel: ObservableObject {
}
func replaceChat(_ id: String, _ chat: Chat) {
if let ix = chats.firstIndex(where: { $0.id == id }) {
chats[ix] = chat
if let i = getChatIndex(id) {
chats[i] = chat
} else {
// invalid state, correcting
chats.insert(chat, at: 0)
@@ -73,23 +73,101 @@ final class ChatModel: ObservableObject {
}
func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
if let ix = chats.firstIndex(where: { $0.id == cInfo.id }) {
chats[ix].chatItems = [cItem]
if ix > 0 {
// update previews
if let i = getChatIndex(cInfo.id) {
chats[i].chatItems = [cItem]
if case .rcvNew = cItem.meta.itemStatus {
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount + 1
}
if i > 0 {
if chatId == nil {
withAnimation { popChat(ix) }
withAnimation { popChat_(i) }
} else if chatId == cInfo.id {
chatToTop = cInfo.id
} else {
DispatchQueue.main.async { self.popChat(ix) }
popChat_(i)
}
}
} else {
addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
}
// add to current chat
if chatId == cInfo.id {
withAnimation { chatItems.append(cItem) }
if case .rcvNew = cItem.meta.itemStatus {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if self.chatId == cInfo.id {
SimpleX.markChatItemRead(cInfo, cItem)
}
}
}
}
}
func upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
// update previews
var res: Bool
if let chat = getChat(cInfo.id) {
if let pItem = chat.chatItems.last, pItem.id == cItem.id {
chat.chatItems = [cItem]
}
res = false
} else {
addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
res = true
}
// update current chat
if chatId == cInfo.id {
withAnimation { chatItems.append(cItem) }
if let i = chatItems.firstIndex(where: { $0.id == cItem.id }) {
withAnimation(.default) {
self.chatItems[i] = cItem
}
return false
} else {
withAnimation { chatItems.append(cItem) }
return true
}
} else {
return res
}
}
private func popChat(_ ix: Int) {
let chat = chats.remove(at: ix)
func markChatItemsRead(_ cInfo: ChatInfo) {
// update preview
if let chat = getChat(cInfo.id) {
chat.chatStats = ChatStats()
}
// update current chat
if chatId == cInfo.id {
var i = 0
while i < chatItems.count {
if case .rcvNew = chatItems[i].meta.itemStatus {
chatItems[i].meta.itemStatus = .rcvRead
}
i = i + 1
}
}
}
func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) {
// update preview
if let i = getChatIndex(cInfo.id) {
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1
}
// update current chat
if chatId == cInfo.id, let j = chatItems.firstIndex(where: { $0.id == cItem.id }) {
chatItems[j].meta.itemStatus = .rcvRead
}
}
func popChat(_ id: String) {
if let i = getChatIndex(id) {
popChat_(i)
}
}
private func popChat_(_ i: Int) {
let chat = chats.remove(at: i)
chats.insert(chat, at: 0)
}
@@ -107,14 +185,6 @@ struct User: Decodable, NamedChat {
var profile: Profile
var activeUser: Bool
// internal init(userId: Int64, userContactId: Int64, localDisplayName: ContactName, profile: Profile, activeUser: Bool) {
// self.userId = userId
// self.userContactId = userContactId
// self.localDisplayName = localDisplayName
// self.profile = profile
// self.activeUser = activeUser
// }
var displayName: String { get { profile.displayName } }
var fullName: String { get { profile.fullName } }
@@ -226,6 +296,16 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
var ready: Bool {
get {
switch self {
case let .direct(contact): return contact.ready
case let .group(groupInfo): return groupInfo.ready
case let .contactRequest(contactRequest): return contactRequest.ready
}
}
}
var createdAt: Date {
switch self {
case let .direct(contact): return contact.createdAt
@@ -250,6 +330,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
final class Chat: ObservableObject, Identifiable {
@Published var chatInfo: ChatInfo
@Published var chatItems: [ChatItem]
@Published var chatStats: ChatStats
@Published var serverInfo = ServerInfo(networkStatus: .unknown)
struct ServerInfo: Decodable {
@@ -297,11 +378,13 @@ final class Chat: ObservableObject, Identifiable {
init(_ cData: ChatData) {
self.chatInfo = cData.chatInfo
self.chatItems = cData.chatItems
self.chatStats = cData.chatStats
}
init(chatInfo: ChatInfo, chatItems: [ChatItem] = []) {
init(chatInfo: ChatInfo, chatItems: [ChatItem] = [], chatStats: ChatStats = ChatStats()) {
self.chatInfo = chatInfo
self.chatItems = chatItems
self.chatStats = chatStats
}
var id: ChatId { get { chatInfo.id } }
@@ -310,10 +393,16 @@ final class Chat: ObservableObject, Identifiable {
struct ChatData: Decodable, Identifiable {
var chatInfo: ChatInfo
var chatItems: [ChatItem]
var chatStats: ChatStats
var id: ChatId { get { chatInfo.id } }
}
struct ChatStats: Decodable {
var unreadCount: Int = 0
var minUnreadItemId: Int64 = 0
}
struct Contact: Identifiable, Decodable, NamedChat {
var contactId: Int64
var localDisplayName: ContactName
@@ -351,6 +440,7 @@ struct UserContactRequest: Decodable, NamedChat {
var id: ChatId { get { "<@\(contactRequestId)" } }
var apiId: Int64 { get { contactRequestId } }
var ready: Bool { get { true } }
var displayName: String { get { profile.displayName } }
var fullName: String { get { profile.fullName } }
@@ -370,6 +460,7 @@ struct GroupInfo: Identifiable, Decodable, NamedChat {
var id: ChatId { get { "#\(groupId)" } }
var apiId: Int64 { get { groupId } }
var ready: Bool { get { true } }
var displayName: String { get { groupProfile.displayName } }
var fullName: String { get { groupProfile.fullName } }
@@ -424,10 +515,17 @@ struct ChatItem: Identifiable, Decodable {
var id: Int64 { get { meta.itemId } }
static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String) -> ChatItem {
var timestampText: String { get { meta.timestampText } }
func isRcvNew() -> Bool {
if case .rcvNew = meta.itemStatus { return true }
return false
}
static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew) -> ChatItem {
ChatItem(
chatDir: dir,
meta: CIMeta.getSample(id, ts, text),
meta: CIMeta.getSample(id, ts, text, status),
content: .sndMsgContent(msgContent: .text(text))
)
}
@@ -455,23 +553,32 @@ struct CIMeta: Decodable {
var itemId: Int64
var itemTs: Date
var itemText: String
var itemStatus: CIStatus
var createdAt: Date
static func getSample(_ id: Int64, _ ts: Date, _ text: String) -> CIMeta {
var timestampText: String { get { SimpleX.timestampText(itemTs) } }
static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew) -> CIMeta {
CIMeta(
itemId: id,
itemTs: ts,
itemText: text,
itemStatus: status,
createdAt: ts
)
}
}
func timestampText(_ date: Date) -> String {
date.formatted(date: .omitted, time: .shortened)
}
enum CIStatus: Decodable {
case sndNew
case sndSent
case sndErrorAuth
case sndError(agentErrorType: AgentErrorType)
case sndError(agentError: AgentErrorType)
case rcvNew
case rcvRead
}
@@ -479,18 +586,25 @@ enum CIStatus: Decodable {
enum CIContent: Decodable {
case sndMsgContent(msgContent: MsgContent)
case rcvMsgContent(msgContent: MsgContent)
// files etc.
case sndFileInvitation(fileId: Int64, filePath: String)
case rcvFileInvitation(rcvFileTransfer: RcvFileTransfer)
var text: String {
get {
switch self {
case let .sndMsgContent(mc): return mc.text
case let .rcvMsgContent(mc): return mc.text
case .sndFileInvitation: return "sending files is not supported yet"
case .rcvFileInvitation: return "receiving files is not supported yet"
}
}
}
}
struct RcvFileTransfer: Decodable {
}
enum MsgContent {
case text(String)
case unknown(type: String, text: String)

View File

@@ -31,6 +31,7 @@ enum ChatCommand {
case showMyAddress
case apiAcceptContact(contactReqId: Int64)
case apiRejectContact(contactReqId: Int64)
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
case string(String)
var cmdString: String {
@@ -40,21 +41,50 @@ enum ChatCommand {
case let .createActiveUser(profile): return "/u \(profile.displayName) \(profile.fullName)"
case .startChat: return "/_start"
case .apiGetChats: return "/_get chats"
case let .apiGetChat(type, id): return "/_get chat \(type.rawValue)\(id) count=500"
case let .apiSendMessage(type, id, mc): return "/_send \(type.rawValue)\(id) \(mc.cmdString)"
case let .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100"
case let .apiSendMessage(type, id, mc): return "/_send \(ref(type, id)) \(mc.cmdString)"
case .addContact: return "/connect"
case let .connect(connReq): return "/connect \(connReq)"
case let .apiDeleteChat(type, id): return "/_delete \(type.rawValue)\(id)"
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
case let .updateProfile(profile): return "/profile \(profile.displayName) \(profile.fullName)"
case .createMyAddress: return "/address"
case .deleteMyAddress: return "/delete_address"
case .showMyAddress: return "/show_address"
case let .apiAcceptContact(contactReqId): return "/_accept \(contactReqId)"
case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)"
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
case let .string(str): return str
}
}
}
var cmdType: String {
get {
switch self {
case .showActiveUser: return "showActiveUser"
case .createActiveUser: return "createActiveUser"
case .startChat: return "startChat"
case .apiGetChats: return "apiGetChats"
case .apiGetChat: return "apiGetChat"
case .apiSendMessage: return "apiSendMessage"
case .addContact: return "addContact"
case .connect: return "connect"
case .apiDeleteChat: return "apiDeleteChat"
case .updateProfile: return "updateProfile"
case .createMyAddress: return "createMyAddress"
case .deleteMyAddress: return "deleteMyAddress"
case .showMyAddress: return "showMyAddress"
case .apiAcceptContact: return "apiAcceptContact"
case .apiRejectContact: return "apiRejectContact"
case .apiChatRead: return "apiChatRead"
case .string: return "console command"
}
}
}
func ref(_ type: ChatType, _ id: Int64) -> String {
"\(type.rawValue)\(id)"
}
}
struct APIResponse: Decodable {
@@ -88,6 +118,8 @@ enum ChatResponse: Decodable, Error {
case groupEmpty(groupInfo: GroupInfo)
case userContactLinkSubscribed
case newChatItem(chatItem: AChatItem)
case chatItemUpdated(chatItem: AChatItem)
case cmdOk
case chatCmdError(chatError: ChatError)
case chatError(chatError: ChatError)
@@ -120,6 +152,8 @@ enum ChatResponse: Decodable, Error {
case .groupEmpty: return "groupEmpty"
case .userContactLinkSubscribed: return "userContactLinkSubscribed"
case .newChatItem: return "newChatItem"
case .chatItemUpdated: return "chatItemUpdated"
case .cmdOk: return "cmdOk"
case .chatCmdError: return "chatCmdError"
case .chatError: return "chatError"
}
@@ -155,6 +189,8 @@ enum ChatResponse: Decodable, Error {
case let .groupEmpty(groupInfo): return String(describing: groupInfo)
case .userContactLinkSubscribed: return noDetails
case let .newChatItem(chatItem): return String(describing: chatItem)
case let .chatItemUpdated(chatItem): return String(describing: chatItem)
case .cmdOk: return noDetails
case let .chatCmdError(chatError): return String(describing: chatError)
case let .chatError(chatError): return String(describing: chatError)
}
@@ -198,7 +234,9 @@ enum TerminalItem: Identifiable {
func chatSendCmd(_ cmd: ChatCommand) throws -> ChatResponse {
var c = cmd.cmdString.cString(using: .utf8)!
logger.debug("chatSendCmd \(cmd.cmdType)")
let resp = chatResponse(chat_send_cmd(getChatCtrl(), &c)!)
logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)")
DispatchQueue.main.async {
ChatModel.shared.terminalItems.append(.cmd(.now, cmd))
ChatModel.shared.terminalItems.append(.resp(.now, resp))
@@ -315,6 +353,12 @@ func apiRejectContactRequest(contactReqId: Int64) throws {
throw r
}
func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) throws {
let r = try chatSendCmd(.apiChatRead(type: type, id: id, itemRange: itemRange))
if case .cmdOk = r { return }
throw r
}
func acceptContactRequest(_ contactRequest: UserContactRequest) {
do {
let contact = try apiAcceptContactRequest(contactReqId: contactRequest.apiId)
@@ -334,6 +378,27 @@ func rejectContactRequest(_ contactRequest: UserContactRequest) {
}
}
func markChatRead(_ chat: Chat) {
do {
let minItemId = chat.chatStats.minUnreadItemId
let itemRange = (minItemId, chat.chatItems.last?.id ?? minItemId)
let cInfo = chat.chatInfo
try apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange)
ChatModel.shared.markChatItemsRead(cInfo)
} catch {
logger.error("markChatRead apiChatRead error: \(error.localizedDescription)")
}
}
func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) {
do {
try apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id))
ChatModel.shared.markChatItemRead(cInfo, cItem)
} catch {
logger.error("markChatItemRead apiChatRead error: \(error.localizedDescription)")
}
}
func initializeChat() {
do {
ChatModel.shared.currentUser = try apiGetActiveUser()
@@ -419,6 +484,12 @@ func processReceivedMsg(_ res: ChatResponse) {
let cItem = aChatItem.chatItem
chatModel.addChatItem(cInfo, cItem)
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
case let .chatItemUpdated(aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
if chatModel.upsertChatItem(cInfo, cItem) {
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
}
default:
logger.debug("unsupported event: \(res.responseType)")
}

View File

@@ -28,7 +28,6 @@ struct SimpleXApp: App {
.onOpenURL { url in
logger.debug("ContentView.onOpenURL: \(url)")
chatModel.appOpenUrl = url
chatModel.connectViaUrl = true
}
.onAppear() {
initializeChat()

View File

@@ -0,0 +1,43 @@
//
// ChatInfoToolbar.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 11/02/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
private let chatImageColorLight = Color(red: 0.9, green: 0.9, blue: 0.9)
private let chatImageColorDark = Color(red: 0.2, green: 0.2, blue: 0.2 )
struct ChatInfoToolbar: View {
@Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat
var body: some View {
let cInfo = chat.chatInfo
return HStack {
ChatInfoImage(
chat: chat,
color: colorScheme == .dark
? chatImageColorDark
: chatImageColorLight
)
.frame(width: 32, height: 32)
.padding(.trailing, 4)
VStack {
Text(cInfo.displayName).font(.headline)
if cInfo.fullName != "" && cInfo.displayName != cInfo.fullName {
Text(cInfo.fullName).font(.subheadline)
}
}
}
.foregroundColor(.primary)
}
}
struct ChatInfoToolbar_Previews: PreviewProvider {
static var previews: some View {
ChatInfoToolbar(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []))
}
}

View File

@@ -10,11 +10,9 @@ import SwiftUI
struct ChatInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var alertManager = AlertManager.shared
@ObservedObject var chat: Chat
@Binding var showChatInfo: Bool
@State private var showDeleteContactAlert = false
@State private var alertContact: Contact?
@State private var showNetworkStatusInfo = false
var body: some View {
VStack{
@@ -30,36 +28,27 @@ struct ChatInfoView: View {
if case let .direct(contact) = chat.chatInfo {
VStack {
HStack {
Button {
showNetworkStatusInfo.toggle()
} label: {
serverImage()
Text(chat.serverInfo.networkStatus.statusString)
.foregroundColor(.primary)
}
}
if showNetworkStatusInfo {
Text(chat.serverInfo.networkStatus.statusExplanation)
.font(.subheadline)
.multilineTextAlignment(.center)
.padding(.horizontal, 64)
.padding(.vertical, 8)
serverImage()
Text(chat.serverInfo.networkStatus.statusString)
.foregroundColor(.primary)
}
Text(chat.serverInfo.networkStatus.statusExplanation)
.font(.subheadline)
.multilineTextAlignment(.center)
.padding(.horizontal, 64)
.padding(.vertical, 8)
Spacer()
Button(role: .destructive) {
alertContact = contact
showDeleteContactAlert = true
alertManager.showAlert(deleteContactAlert(contact))
} label: {
Label("Delete contact", systemImage: "trash")
}
.padding()
.alert(isPresented: $showDeleteContactAlert) {
deleteContactAlert(alertContact!)
}
}
}
}
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
@@ -81,10 +70,8 @@ struct ChatInfoView: View {
} catch let error {
logger.error("ChatInfoView.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)")
}
alertContact = nil
}, secondaryButton: .cancel() {
alertContact = nil
}
},
secondaryButton: .cancel()
)
}
}

View File

@@ -0,0 +1,47 @@
//
// CIMetaView.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 11/02/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct CIMetaView: View {
var chatItem: ChatItem
var body: some View {
HStack(alignment: .center, spacing: 4) {
switch chatItem.meta.itemStatus {
case .sndSent:
statusImage("checkmark", .secondary)
case .sndErrorAuth:
statusImage("multiply", .red)
case .sndError:
statusImage("exclamationmark.triangle.fill", .yellow)
case .rcvNew:
statusImage("circlebadge.fill", Color.accentColor)
default: EmptyView()
}
Text(chatItem.timestampText)
.font(.caption)
.foregroundColor(.secondary)
}
}
private func statusImage(_ systemName: String, _ color: Color) -> some View {
Image(systemName: systemName)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(color)
.frame(maxHeight: 8)
}
}
struct CIMetaView_Previews: PreviewProvider {
static var previews: some View {
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent))
}
}

View File

@@ -14,15 +14,13 @@ struct EmojiItemView: View {
var body: some View {
let sent = chatItem.chatDir.sent
VStack {
VStack(spacing: 1) {
Text(chatItem.content.text.trimmingCharacters(in: .whitespaces))
.font(emojiFont)
.padding(.top, 8)
.padding(.horizontal, 6)
.frame(maxWidth: .infinity, alignment: sent ? .trailing : .leading)
Text(getDateFormatter().string(from: chatItem.meta.itemTs))
.font(.caption)
.foregroundColor(.secondary)
CIMetaView(chatItem: chatItem)
.padding(.bottom, 8)
.padding(.horizontal, 12)
.frame(maxWidth: .infinity, alignment: sent ? .trailing : .leading)
@@ -35,7 +33,7 @@ struct EmojiItemView: View {
struct EmojiItemView_Previews: PreviewProvider {
static var previews: some View {
Group{
EmojiItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"))
EmojiItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent))
EmojiItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "👍"))
}
.previewLayout(.fixed(width: 360, height: 70))

View File

@@ -12,7 +12,7 @@ private let emailRegex = try! NSRegularExpression(pattern: "^[a-z0-9.!#$%&'*+/=?
private let phoneRegex = try! NSRegularExpression(pattern: "^\\+?[0-9\\.\\(\\)\\-]{7,20}$")
private let sentColorLigth = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12)
private let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12)
private let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17)
private let linkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
@@ -24,30 +24,22 @@ struct TextItemView: View {
var body: some View {
let sent = chatItem.chatDir.sent
// let minWidth = min(200, width)
let maxWidth = width * 0.78
let meta = getDateFormatter().string(from: chatItem.meta.itemTs)
return ZStack(alignment: .bottomTrailing) {
(messageText(chatItem) + reserveSpaceForMeta(meta))
.padding(.top, 6)
.padding(.bottom, 7)
(messageText(chatItem) + reserveSpaceForMeta(chatItem.timestampText))
.padding(.vertical, 6)
.padding(.horizontal, 12)
.frame(minWidth: 0, alignment: .leading)
// .foregroundColor(sent ? .white : .primary)
.textSelection(.enabled)
Text(meta)
.font(.caption)
.foregroundColor(.secondary)
// .foregroundColor(sent ? Color(uiColor: .secondarySystemBackground) : .secondary)
.padding(.bottom, 4)
.padding(.horizontal, 12)
CIMetaView(chatItem: chatItem)
.padding(.trailing, 12)
.padding(.bottom, 6)
}
// .background(sent ? .blue : Color(uiColor: .tertiarySystemGroupedBackground))
.background(
sent
? (colorScheme == .light ? sentColorLigth : sentColorDark)
? (colorScheme == .light ? sentColorLight : sentColorDark)
: Color(uiColor: .tertiarySystemGroupedBackground)
)
.cornerRadius(18)
@@ -57,6 +49,13 @@ struct TextItemView: View {
maxHeight: .infinity,
alignment: sent ? .trailing : .leading
)
.onTapGesture {
switch chatItem.meta.itemStatus {
case .sndErrorAuth: msgDeliveryError("Most likely this contact has deleted the connection with you.")
case let .sndError(agentError): msgDeliveryError("Unexpected error: \(String(describing: agentError))")
default: return
}
}
}
private func messageText(_ chatItem: ChatItem) -> Text {
@@ -82,10 +81,9 @@ struct TextItemView: View {
}
private func reserveSpaceForMeta(_ meta: String) -> Text {
Text(AttributedString(" \(meta)", attributes: AttributeContainer([
.font: UIFont.preferredFont(forTextStyle: .caption1) as Any,
.foregroundColor: UIColor.clear as Any,
])))
Text(" \(meta)")
.font(.caption)
.foregroundColor(.clear)
}
private func wordToText(_ s: String.SubSequence) -> Text {
@@ -126,6 +124,13 @@ struct TextItemView: View {
private func mdText(_ s: String.SubSequence) -> Text {
Text(s[s.index(s.startIndex, offsetBy: 1)..<s.index(s.endIndex, offsetBy: -1)])
}
private func msgDeliveryError(_ err: String) {
AlertManager.shared.showAlertMsg(
title: "Message delivery error",
message: err
)
}
}
struct TextItemView_Previews: PreviewProvider {
@@ -133,7 +138,7 @@ struct TextItemView_Previews: PreviewProvider {
Group{
TextItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), width: 360)
TextItemView(chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello"), width: 360)
TextItemView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat"), width: 360)
TextItemView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent), width: 360)
TextItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), width: 360)
TextItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), width: 360)
TextItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), width: 360)

View File

@@ -8,9 +8,6 @@
import SwiftUI
private let chatImageColorLight = Color(red: 0.9, green: 0.9, blue: 0.9)
private let chatImageColorDark = Color(red: 0.2, green: 0.2, blue: 0.2 )
struct ChatView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.colorScheme) var colorScheme
@@ -31,8 +28,15 @@ struct ChatView: View {
ChatItemView(chatItem: $0, width: g.size.width)
.frame(minWidth: 0, maxWidth: .infinity, alignment: $0.chatDir.sent ? .trailing : .leading)
}
.onAppear { scrollToBottom(proxy) }
.onChange(of: chatModel.chatItems.count) { _ in scrollToBottom(proxy) }
.onAppear {
DispatchQueue.main.async {
scrollToFirstUnread(proxy)
}
markAllRead()
}
.onChange(of: chatModel.chatItems.count) { _ in
scrollToBottom(proxy)
}
.onChange(of: keyboardVisible) { _ in
if keyboardVisible {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
@@ -42,7 +46,6 @@ struct ChatView: View {
}
}
}
.coordinateSpace(name: "scrollView")
.onTapGesture {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
@@ -72,23 +75,7 @@ struct ChatView: View {
Button {
showChatInfo = true
} label: {
HStack {
ChatInfoImage(
chat: chat,
color: colorScheme == .dark
? chatImageColorDark
: chatImageColorLight
)
.frame(width: 32, height: 32)
.padding(.trailing, 4)
VStack {
Text(cInfo.displayName).font(.headline)
if cInfo.fullName != "" && cInfo.displayName != cInfo.fullName {
Text(cInfo.fullName).font(.subheadline)
}
}
}
.foregroundColor(.primary)
ChatInfoToolbar(chat: chat)
}
.sheet(isPresented: $showChatInfo) {
ChatInfoView(chat: chat, showChatInfo: $showChatInfo)
@@ -99,9 +86,28 @@ struct ChatView: View {
}
func scrollToBottom(_ proxy: ScrollViewProxy, animation: Animation = .default) {
withAnimation(animation) { scrollToBottom_(proxy) }
}
func scrollToBottom_(_ proxy: ScrollViewProxy) {
if let id = chatModel.chatItems.last?.id {
withAnimation(animation) {
proxy.scrollTo(id, anchor: .bottom)
proxy.scrollTo(id, anchor: .bottom)
}
}
// align first unread with the top or the last unread with bottom
func scrollToFirstUnread(_ proxy: ScrollViewProxy) {
if let cItem = chatModel.chatItems.first(where: { $0.isRcvNew() }) {
proxy.scrollTo(cItem.id)
} else {
scrollToBottom_(proxy)
}
}
func markAllRead() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if chatModel.chatId == chat.id {
markChatRead(chat)
}
}
}

View File

@@ -11,14 +11,7 @@ import SwiftUI
struct ChatListNavLink: View {
@EnvironmentObject var chatModel: ChatModel
@State var chat: Chat
@State private var showDeleteContactAlert = false
@State private var showDeleteGroupAlert = false
@State private var showContactRequestAlert = false
@State private var showContactRequestDialog = false
@State private var alertContact: Contact?
@State private var alertGroupInfo: GroupInfo?
@State private var alertContactRequest: UserContactRequest?
var body: some View {
switch chat.chatInfo {
@@ -46,64 +39,72 @@ struct ChatListNavLink: View {
}
private func contactNavLink(_ contact: Contact) -> some View {
NavigationLink(
NavLinkPlain(
tag: chat.chatInfo.id,
selection: $chatModel.chatId,
destination: { chatView() },
label: { ChatPreviewView(chat: chat) }
label: { ChatPreviewView(chat: chat) },
disabled: !contact.ready
)
.disabled(!contact.ready)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
.swipeActions(edge: .leading) {
if chat.chatStats.unreadCount > 0 {
markReadButton()
}
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
alertContact = contact
showDeleteContactAlert = true
AlertManager.shared.showAlert(deleteContactAlert(contact))
} label: {
Label("Delete", systemImage: "trash")
}
}
.alert(isPresented: $showDeleteContactAlert) {
deleteContactAlert(alertContact!)
}
.frame(height: 80)
}
private func groupNavLink(_ groupInfo: GroupInfo) -> some View {
NavigationLink(
NavLinkPlain(
tag: chat.chatInfo.id,
selection: $chatModel.chatId,
destination: { chatView() },
label: { ChatPreviewView(chat: chat) }
label: { ChatPreviewView(chat: chat) },
disabled: !groupInfo.ready
)
.swipeActions(edge: .leading) {
if chat.chatStats.unreadCount > 0 {
markReadButton()
}
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
alertGroupInfo = groupInfo
showDeleteGroupAlert = true
AlertManager.shared.showAlert(deleteGroupAlert(groupInfo))
} label: {
Label("Delete", systemImage: "trash")
}
}
.alert(isPresented: $showDeleteGroupAlert) {
deleteGroupAlert(alertGroupInfo!)
}
.frame(height: 80)
}
private func markReadButton() -> some View {
Button {
markChatRead(chat)
} label: {
Label("Read", systemImage: "checkmark")
}
.tint(Color.accentColor)
}
private func contactRequestNavLink(_ contactRequest: UserContactRequest) -> some View {
ContactRequestView(contactRequest: contactRequest)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button { acceptContactRequest(contactRequest) }
label: { Label("Accept", systemImage: "checkmark") }
.tint(.blue)
.tint(Color.accentColor)
Button(role: .destructive) {
alertContactRequest = contactRequest
showContactRequestAlert = true
AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest))
} label: {
Label("Reject", systemImage: "multiply")
}
}
.alert(isPresented: $showContactRequestAlert) {
contactRequestAlert(alertContactRequest!)
}
.frame(height: 80)
.onTapGesture { showContactRequestDialog = true }
.confirmationDialog("Connection request", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
@@ -123,10 +124,8 @@ struct ChatListNavLink: View {
} catch let error {
logger.error("ChatListNavLink.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)")
}
alertContact = nil
}, secondaryButton: .cancel() {
alertContact = nil
}
},
secondaryButton: .cancel()
)
}
@@ -137,16 +136,14 @@ struct ChatListNavLink: View {
)
}
private func contactRequestAlert(_ contactRequest: UserContactRequest) -> Alert {
private func rejectContactRequestAlert(_ contactRequest: UserContactRequest) -> Alert {
Alert(
title: Text("Reject contact request"),
message: Text("The sender will NOT be notified"),
primaryButton: .destructive(Text("Reject")) {
rejectContactRequest(contactRequest)
alertContactRequest = nil
}, secondaryButton: .cancel {
alertContactRequest = nil
}
},
secondaryButton: .cancel()
)
}
}

View File

@@ -10,8 +10,6 @@ import SwiftUI
struct ChatListView: View {
@EnvironmentObject var chatModel: ChatModel
@State private var connectAlert = false
@State private var connectError: Error?
// not really used in this view
@State private var showSettings = false
@@ -32,6 +30,19 @@ struct ChatListView: View {
}
ForEach(chatModel.chats) { chat in
ChatListNavLink(chat: chat)
.padding(.trailing, -16)
}
}
.onChange(of: chatModel.chatId) { _ in
if chatModel.chatId == nil, let chatId = chatModel.chatToTop {
chatModel.chatToTop = nil
chatModel.popChat(chatId)
}
}
.onChange(of: chatModel.appOpenUrl) { _ in
if let url = chatModel.appOpenUrl {
chatModel.appOpenUrl = nil
AlertManager.shared.showAlert(connectViaUrlAlert(url))
}
}
.offset(x: -8)
@@ -45,50 +56,36 @@ struct ChatListView: View {
NewChatButton()
}
}
.alert(isPresented: $chatModel.connectViaUrl) { connectViaUrlAlert() }
}
.navigationViewStyle(.stack)
.alert(isPresented: $connectAlert) { connectionErrorAlert() }
}
private func connectViaUrlAlert() -> Alert {
logger.debug("ChatListView.connectViaUrlAlert")
if let url = chatModel.appOpenUrl {
var path = url.path
logger.debug("ChatListView.connectViaUrlAlert path: \(path)")
if (path == "/contact" || path == "/invitation") {
path.removeFirst()
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
return Alert(
title: Text("Connect via \(path) link?"),
message: Text("Your profile will be sent to the contact that you received this link from: \(link)"),
primaryButton: .default(Text("Connect")) {
private func connectViaUrlAlert(_ url: URL) -> Alert {
var path = url.path
logger.debug("ChatListView.connectViaUrlAlert path: \(path)")
if (path == "/contact" || path == "/invitation") {
path.removeFirst()
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
return Alert(
title: Text("Connect via \(path) link?"),
message: Text("Your profile will be sent to the contact that you received this link from: \(link)"),
primaryButton: .default(Text("Connect")) {
DispatchQueue.main.async {
do {
try apiConnect(connReq: link)
} catch {
connectAlert = true
connectError = error
logger.debug("ChatListView.connectViaUrlAlert: apiConnect error: \(error.localizedDescription)")
let err = error.localizedDescription
AlertManager.shared.showAlertMsg(title: "Connection error", message: err)
logger.debug("ChatListView.connectViaUrlAlert: apiConnect error: \(err)")
}
chatModel.appOpenUrl = nil
}, secondaryButton: .cancel() {
chatModel.appOpenUrl = nil
}
)
} else {
return Alert(title: Text("Error: URL is invalid"))
}
},
secondaryButton: .cancel()
)
} else {
return Alert(title: Text("Error: URL not available"))
return Alert(title: Text("Error: URL is invalid"))
}
}
private func connectionErrorAlert() -> Alert {
Alert(
title: Text("Connection error"),
message: Text(connectError?.localizedDescription ?? "")
)
}
}
struct ChatListView_Previews: PreviewProvider {

View File

@@ -15,6 +15,7 @@ struct ChatPreviewView: View {
var body: some View {
let cItem = chat.chatItems.last
let unread = chat.chatStats.unreadCount
return HStack(spacing: 8) {
ZStack(alignment: .bottomLeading) {
ChatInfoImage(chat: chat)
@@ -35,21 +36,36 @@ struct ChatPreviewView: View {
Text(chat.chatInfo.chatViewName)
.font(.title3)
.fontWeight(.bold)
.foregroundColor(chat.chatInfo.ready ? .primary : .secondary)
.frame(maxHeight: .infinity, alignment: .topLeading)
Spacer()
Text(getDateFormatter().string(from: cItem?.meta.itemTs ?? chat.chatInfo.createdAt))
Text(cItem?.timestampText ?? timestampText(chat.chatInfo.createdAt))
.font(.subheadline)
.frame(minWidth: 60, alignment: .trailing)
.foregroundColor(.secondary)
.padding(.top, 4)
}
.padding(.top, 4)
.padding(.horizontal, 8)
if let cItem = cItem {
Text(chatItemText(cItem))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading)
.padding([.leading, .trailing], 8)
.padding(.bottom, 4)
ZStack(alignment: .topTrailing) {
(itemStatusMark(cItem) + Text(chatItemText(cItem)))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading)
.padding(.leading, 8)
.padding(.trailing, 32)
.padding(.bottom, 4)
if unread > 0 {
Text(unread > 9 ? "" : "\(unread)")
.font(.caption)
.foregroundColor(.white)
.frame(width: 18, height: 18)
.background(Color.accentColor) //Color(.sRGB, red: 0, green: 0.57, blue: 1, opacity: 0.89))
.cornerRadius(10)
}
}
.padding(.trailing, 8)
}
else if case let .direct(contact) = chat.chatInfo, !contact.ready {
Text("Connecting...")
@@ -61,6 +77,20 @@ struct ChatPreviewView: View {
}
}
private func itemStatusMark(_ cItem: ChatItem) -> Text {
switch cItem.meta.itemStatus {
case .sndErrorAuth:
return Text(Image(systemName: "multiply"))
.font(.caption)
.foregroundColor(.red) + Text(" ")
case .sndError:
return Text(Image(systemName: "exclamationmark.triangle.fill"))
.font(.caption)
.foregroundColor(.yellow) + Text(" ")
default: return Text("")
}
}
private func chatItemText(_ cItem: ChatItem) -> String {
let t = cItem.content.text
if case let .groupRcv(groupMember) = cItem.chatDir {
@@ -79,11 +109,12 @@ struct ChatPreviewView_Previews: PreviewProvider {
))
ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)]
))
ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.group,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")]
chatItems: [ChatItem.getSample(1, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")],
chatStats: ChatStats(unreadCount: 3, minUnreadItemId: 0)
))
}
.previewLayout(.fixed(width: 360, height: 78))

View File

@@ -28,9 +28,9 @@ struct ContactRequestView: View {
.padding(.top, 4)
.frame(maxHeight: .infinity, alignment: .topLeading)
Spacer()
Text(getDateFormatter().string(from: contactRequest.createdAt))
Text(timestampText(contactRequest.createdAt))
.font(.subheadline)
.padding(.trailing, 28)
.padding(.trailing, 8)
.padding(.top, 4)
.frame(minWidth: 60, alignment: .trailing)
.foregroundColor(.secondary)

View File

@@ -0,0 +1,35 @@
//
// NavLinkPlain.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 11/02/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct NavLinkPlain<V: Hashable, Destination: View, Label: View>: View {
@State var tag: V
@Binding var selection: V?
@ViewBuilder var destination: () -> Destination
@ViewBuilder var label: () -> Label
var disabled = false
var body: some View {
ZStack {
Button("") { selection = tag }
.disabled(disabled)
label()
}
.background {
NavigationLink("", tag: tag, selection: $selection, destination: destination)
.hidden()
}
}
}
//struct NavLinkPlain_Previews: PreviewProvider {
// static var previews: some View {
// NavLinkPlain()
// }
//}

View File

@@ -0,0 +1,18 @@
//
// ShareSheet.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 30/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
func showShareSheet(items: [Any]) {
let keyWindowScene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene
if let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first,
let presentedViewController = keyWindow.rootViewController?.presentedViewController ?? keyWindow.rootViewController {
let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
presentedViewController.present(activityViewController, animated: true)
}
}

View File

@@ -11,7 +11,6 @@ import CoreImage.CIFilterBuiltins
struct AddContactView: View {
var connReqInvitation: String
@State private var shareInvitation = false
var body: some View {
VStack {
@@ -27,11 +26,12 @@ struct AddContactView: View {
.font(.subheadline)
.multilineTextAlignment(.center)
.padding(.horizontal)
Button { shareInvitation = true } label: {
Label("Share", systemImage: "square.and.arrow.up")
Button {
showShareSheet(items: [connReqInvitation])
} label: {
Label("Share invitation link", systemImage: "square.and.arrow.up")
}
.padding()
.shareSheet(isPresented: $shareInvitation, items: [connReqInvitation])
}
}
}

View File

@@ -11,12 +11,8 @@ import SwiftUI
struct NewChatButton: View {
@State private var showAddChat = false
@State private var addContact = false
@State private var addContactAlert = false
@State private var addContactError: Error?
@State private var connReqInvitation: String = ""
@State private var connectContact = false
@State private var connectAlert = false
@State private var connectError: Error?
@State private var createGroup = false
var body: some View {
@@ -32,15 +28,9 @@ struct NewChatButton: View {
.sheet(isPresented: $addContact, content: {
AddContactView(connReqInvitation: connReqInvitation)
})
.alert(isPresented: $addContactAlert) {
connectionError(addContactError)
}
.sheet(isPresented: $connectContact, content: {
connectContactSheet()
})
.alert(isPresented: $connectAlert) {
connectionError(connectError)
}
.sheet(isPresented: $createGroup, content: { CreateGroupView() })
}
@@ -49,8 +39,7 @@ struct NewChatButton: View {
connReqInvitation = try apiAddContact()
addContact = true
} catch {
addContactAlert = true
addContactError = error
connectionErrorAlert(error)
logger.error("NewChatButton.addContactAction apiAddContact error: \(error.localizedDescription)")
}
}
@@ -58,18 +47,14 @@ struct NewChatButton: View {
func connectContactSheet() -> some View {
ConnectContactView(completed: { err in
connectContact = false
if err != nil {
connectAlert = true
connectError = err
if let error = err {
connectionErrorAlert(error)
}
})
}
func connectionError(_ error: Error?) -> Alert {
Alert(
title: Text("Connection error"),
message: Text(error?.localizedDescription ?? "")
)
func connectionErrorAlert(_ error: Error) {
AlertManager.shared.showAlertMsg(title: "Connection error", message: error.localizedDescription)
}
}

View File

@@ -1,40 +0,0 @@
//
// ShareSheet.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 30/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
extension UIApplication {
static let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first
static let keyWindowScene = shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene
}
extension View {
func shareSheet(isPresented: Binding<Bool>, items: [Any]) -> some View {
guard isPresented.wrappedValue else { return self }
let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
let presentedViewController = UIApplication.keyWindow?.rootViewController?.presentedViewController ?? UIApplication.keyWindow?.rootViewController
activityViewController.completionWithItemsHandler = { _, _, _, _ in isPresented.wrappedValue = false }
presentedViewController?.present(activityViewController, animated: true)
return self
}
}
struct ShareSheetTest: View {
@State private var isPresentingShareSheet = false
var body: some View {
Button("Show Share Sheet") { isPresentingShareSheet = true }
.shareSheet(isPresented: $isPresentingShareSheet, items: ["Share me!"])
}
}
struct ShareSheetTest_Previews: PreviewProvider {
static var previews: some View {
ShareSheetTest()
}
}

View File

@@ -8,6 +8,8 @@
import SwiftUI
private let terminalFont = Font.custom("Menlo", size: 16)
struct TerminalView: View {
@EnvironmentObject var chatModel: ChatModel
@State var inProgress: Bool = false
@@ -31,6 +33,7 @@ struct TerminalView: View {
Text(item.label)
.frame(maxWidth: .infinity, maxHeight: 30, alignment: .leading)
}
.font(terminalFont)
.padding(.horizontal)
}
}

View File

@@ -11,6 +11,7 @@ import SwiftUI
let simplexTeamURL = URL(string: "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")!
struct SettingsView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var chatModel: ChatModel
@Binding var showSettings: Bool
@@ -95,7 +96,7 @@ struct SettingsView: View {
}
}
HStack {
Image("github")
Image(colorScheme == .dark ? "github_light" : "github")
.resizable()
.frame(width: 24, height: 24)
.padding(.trailing, 8)

View File

@@ -10,7 +10,6 @@ import SwiftUI
struct UserAddress: View {
@EnvironmentObject var chatModel: ChatModel
@State private var shareAddressLink = false
@State private var deleteAddressAlert = false
var body: some View {
@@ -20,13 +19,14 @@ struct UserAddress: View {
if let userAdress = chatModel.userAddress {
QRCode(uri: userAdress)
HStack {
Button { shareAddressLink = true } label: {
Button {
showShareSheet(items: [userAdress])
} label: {
Label("Share link", systemImage: "square.and.arrow.up")
}
.padding()
.shareSheet(isPresented: $shareAddressLink, items: [userAdress])
Button { deleteAddressAlert = true } label: {
Button(role: .destructive) { deleteAddressAlert = true } label: {
Label("Delete address", systemImage: "trash")
}
.padding()
@@ -44,7 +44,6 @@ struct UserAddress: View {
}, secondaryButton: .cancel()
)
}
.shareSheet(isPresented: $shareAddressLink, items: [userAdress])
}
.frame(maxWidth: .infinity)
} else {

View File

@@ -34,6 +34,12 @@
5C75059E27B5CD9300BE3227 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C75059927B5CD9300BE3227 /* libffi.a */; };
5C75059F27B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C75059A27B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a */; };
5C7505A027B5CD9300BE3227 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C75059B27B5CD9300BE3227 /* libgmpxx.a */; };
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; };
5C7505A327B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; };
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
5C7505A627B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; };
5C7505A927B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; };
5C764E80279C7276000C6508 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E7F279C7276000C6508 /* dummy.m */; };
5C764E81279C7276000C6508 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E7F279C7276000C6508 /* dummy.m */; };
5C764E82279C748B000C6508 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C764E7B279C71D4000C6508 /* libiconv.tbd */; };
@@ -127,6 +133,9 @@
5C75059927B5CD9300BE3227 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C75059A27B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a"; sourceTree = "<group>"; };
5C75059B27B5CD9300BE3227 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = "<group>"; };
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
5C764E7B279C71D4000C6508 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libiconv.tbd; sourceTree = DEVELOPER_DIR; };
5C764E7C279C71DB000C6508 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; };
5C764E7D279C7275000C6508 /* SimpleX (iOS)-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SimpleX (iOS)-Bridging-Header.h"; sourceTree = "<group>"; };
@@ -225,6 +234,7 @@
children = (
5CE4407427ADB657007B033A /* ChatItem */,
5C2E260E27A30FDC00F70299 /* ChatView.swift */,
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */,
5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */,
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */,
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */,
@@ -270,6 +280,8 @@
isa = PBXGroup;
children = (
5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */,
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */,
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */,
);
path = Helpers;
sourceTree = "<group>";
@@ -348,7 +360,6 @@
5CCD403627A5F9A200368C90 /* ConnectContactView.swift */,
5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */,
5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */,
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */,
);
path = NewChat;
sourceTree = "<group>";
@@ -380,6 +391,7 @@
isa = PBXGroup;
children = (
5CE4407527ADB66A007B033A /* TextItemView.swift */,
5C7505A127B65FDB00BE3227 /* CIMetaView.swift */,
5CE4407827ADB701007B033A /* EmojiItemView.swift */,
);
path = ChatItem;
@@ -559,6 +571,7 @@
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */,
5C764E80279C7276000C6508 /* dummy.m in Sources */,
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */,
5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */,
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */,
5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */,
@@ -570,6 +583,8 @@
5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */,
5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */,
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */,
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */,
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */,
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
5CA05A4C27974EB60002BEB4 /* WelcomeView.swift in Sources */,
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
@@ -599,6 +614,7 @@
5CE4407A27ADB701007B033A /* EmojiItemView.swift in Sources */,
5C5346A927B59A6A004DF848 /* ChatHelp.swift in Sources */,
5C764E81279C7276000C6508 /* dummy.m in Sources */,
5C7505A927B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */,
5CB924E527A8683A00ACCCDD /* UserAddress.swift in Sources */,
5C063D2827A4564100AEC577 /* ChatPreviewView.swift in Sources */,
5C35CFCC27B2E91D00FB6C6D /* NtfManager.swift in Sources */,
@@ -610,6 +626,8 @@
5CB9250E27A9432000ACCCDD /* ChatListNavLink.swift in Sources */,
5CA059EE279559F40002BEB4 /* ContentView.swift in Sources */,
5CCD403527A5F6DF00368C90 /* AddContactView.swift in Sources */,
5C7505A627B679EE00BE3227 /* NavLinkPlain.swift in Sources */,
5C7505A327B65FDB00BE3227 /* CIMetaView.swift in Sources */,
5C35CFC927B2782E00FB6C6D /* BGManager.swift in Sources */,
5CA05A4D27974EB60002BEB4 /* WelcomeView.swift in Sources */,
5C2E261027A30FDC00F70299 /* ChatView.swift in Sources */,