ios: verify connection security code (#1542)
* ios: verify connection security code * verification in member sheet (still crashes) * use navigation view for members list * ios: show verified status in the lists * update verification status in the list of members * verified shield layout * update icon, make add member navigation to right * refactor chatPreviewTitle
This commit is contained in:
committed by
GitHub
parent
c77f6100c5
commit
7b4710d198
@@ -356,14 +356,14 @@ func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) a
|
||||
try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings))
|
||||
}
|
||||
|
||||
func apiContactInfo(contactId: Int64) async throws -> (ConnectionStats?, Profile?) {
|
||||
func apiContactInfo(_ contactId: Int64) async throws -> (ConnectionStats?, Profile?) {
|
||||
let r = await chatSendCmd(.apiContactInfo(contactId: contactId))
|
||||
if case let .contactInfo(_, connStats, customUserProfile) = r { return (connStats, customUserProfile) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (ConnectionStats?) {
|
||||
let r = await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
|
||||
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (ConnectionStats?) {
|
||||
let r = chatSendCmdSync(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .groupMemberInfo(_, _, connStats_) = r { return (connStats_) }
|
||||
throw r
|
||||
}
|
||||
@@ -376,6 +376,32 @@ func apiSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) async throws
|
||||
try await sendCommandOkResp(.apiSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId))
|
||||
}
|
||||
|
||||
func apiGetContactCode(_ contactId: Int64) async throws -> (Contact, String) {
|
||||
let r = await chatSendCmd(.apiGetContactCode(contactId: contactId))
|
||||
if case let .contactCode(contact, connectionCode) = r { return (contact, connectionCode) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetGroupMemberCode(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, String) {
|
||||
let r = chatSendCmdSync(.apiGetGroupMemberCode(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .groupMemberCode(_, member, connectionCode) = r { return (member, connectionCode) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiVerifyContact(_ contactId: Int64, connectionCode: String?) -> (Bool, String)? {
|
||||
let r = chatSendCmdSync(.apiVerifyContact(contactId: contactId, connectionCode: connectionCode))
|
||||
if case let .connectionVerified(verified, expectedCode) = r { return (verified, expectedCode) }
|
||||
logger.error("apiVerifyContact error: \(String(describing: r))")
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCode: String?) -> (Bool, String)? {
|
||||
let r = chatSendCmdSync(.apiVerifyGroupMember(groupId: groupId, groupMemberId: groupMemberId, connectionCode: connectionCode))
|
||||
if case let .connectionVerified(verified, expectedCode) = r { return (verified, expectedCode) }
|
||||
logger.error("apiVerifyGroupMember error: \(String(describing: r))")
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiAddContact() async -> String? {
|
||||
let r = await chatSendCmd(.addContact, bgTask: false)
|
||||
if case let .invitation(connReqInvitation) = r { return connReqInvitation }
|
||||
@@ -782,6 +808,12 @@ func apiListMembers(_ groupId: Int64) async -> [GroupMember] {
|
||||
return []
|
||||
}
|
||||
|
||||
func apiListMembersSync(_ groupId: Int64) -> [GroupMember] {
|
||||
let r = chatSendCmdSync(.apiListMembers(groupId: groupId))
|
||||
if case let .groupMembers(group) = r { return group.members }
|
||||
return []
|
||||
}
|
||||
|
||||
func filterMembersToAdd(_ ms: [GroupMember]) -> [Contact] {
|
||||
let memberContactIds = ms.compactMap{ m in m.memberCurrent ? m.memberContactId : nil }
|
||||
return ChatModel.shared.chats
|
||||
|
||||
@@ -32,15 +32,26 @@ struct ChatInfoToolbar: View {
|
||||
.frame(width: imageSize, height: imageSize)
|
||||
.padding(.trailing, 4)
|
||||
VStack {
|
||||
Text(cInfo.displayName).font(.headline)
|
||||
let t = Text(cInfo.displayName).font(.headline)
|
||||
(cInfo.contact?.verified == true ? contactVerifiedShield + t : t)
|
||||
.lineLimit(1)
|
||||
if cInfo.fullName != "" && cInfo.displayName != cInfo.fullName {
|
||||
Text(cInfo.fullName).font(.subheadline)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
.frame(width: 220)
|
||||
}
|
||||
|
||||
private var contactVerifiedShield: Text {
|
||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.baselineOffset(1)
|
||||
.kerning(-2)
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatInfoToolbar_Previews: PreviewProvider {
|
||||
|
||||
@@ -55,8 +55,9 @@ struct ChatInfoView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@State var contact: Contact
|
||||
@Binding var connectionStats: ConnectionStats?
|
||||
var customUserProfile: Profile?
|
||||
@Binding var customUserProfile: Profile?
|
||||
@State var localAlias: String
|
||||
@Binding var connectionCode: String?
|
||||
@FocusState private var aliasTextFieldFocused: Bool
|
||||
@State private var alert: ChatInfoViewAlert? = nil
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@@ -89,7 +90,9 @@ struct ChatInfoView: View {
|
||||
aliasTextFieldFocused = false
|
||||
}
|
||||
|
||||
localAliasTextEdit()
|
||||
Group {
|
||||
localAliasTextEdit()
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
@@ -100,6 +103,7 @@ struct ChatInfoView: View {
|
||||
}
|
||||
|
||||
Section {
|
||||
if let code = connectionCode { verifyCodeButton(code) }
|
||||
contactPreferencesButton()
|
||||
}
|
||||
|
||||
@@ -143,17 +147,23 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func contactInfoHeader() -> some View {
|
||||
private func contactInfoHeader() -> some View {
|
||||
VStack {
|
||||
let cInfo = chat.chatInfo
|
||||
ChatInfoImage(chat: chat, color: Color(uiColor: .tertiarySystemFill))
|
||||
.frame(width: 192, height: 192)
|
||||
.padding(.top, 12)
|
||||
.padding()
|
||||
Text(contact.profile.displayName)
|
||||
.font(.largeTitle)
|
||||
.lineLimit(1)
|
||||
.padding(.bottom, 2)
|
||||
HStack {
|
||||
if contact.verified {
|
||||
Image(systemName: "checkmark.shield")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Text(contact.profile.displayName)
|
||||
.font(.largeTitle)
|
||||
.lineLimit(1)
|
||||
.padding(.bottom, 2)
|
||||
}
|
||||
if cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName {
|
||||
Text(cInfo.fullName)
|
||||
.font(.title2)
|
||||
@@ -163,7 +173,7 @@ struct ChatInfoView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
|
||||
func localAliasTextEdit() -> some View {
|
||||
private func localAliasTextEdit() -> some View {
|
||||
TextField("Set contact name…", text: $localAlias)
|
||||
.disableAutocorrection(true)
|
||||
.focused($aliasTextFieldFocused)
|
||||
@@ -194,7 +204,36 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func contactPreferencesButton() -> some View {
|
||||
private func verifyCodeButton(_ code: String) -> some View {
|
||||
NavigationLink {
|
||||
VerifyCodeView(
|
||||
displayName: contact.displayName,
|
||||
connectionCode: code,
|
||||
connectionVerified: contact.verified,
|
||||
verify: { code in
|
||||
if let r = apiVerifyContact(chat.chatInfo.apiId, connectionCode: code) {
|
||||
let (verified, existingCode) = r
|
||||
contact.activeConn.connectionCode = verified ? SecurityCode(securityCode: existingCode, verifiedAt: .now) : nil
|
||||
connectionCode = existingCode
|
||||
DispatchQueue.main.async {
|
||||
chat.chatInfo = .direct(contact: contact)
|
||||
}
|
||||
return r
|
||||
}
|
||||
return nil
|
||||
}
|
||||
)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Security code")
|
||||
} label: {
|
||||
Label(
|
||||
contact.verified ? "View security code" : "Verify security code",
|
||||
systemImage: contact.verified ? "checkmark.shield" : "shield"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func contactPreferencesButton() -> some View {
|
||||
NavigationLink {
|
||||
ContactPreferencesView(
|
||||
contact: $contact,
|
||||
@@ -208,7 +247,7 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func networkStatusRow() -> some View {
|
||||
private func networkStatusRow() -> some View {
|
||||
HStack {
|
||||
Text("Network status")
|
||||
Image(systemName: "info.circle")
|
||||
@@ -221,14 +260,14 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func serverImage() -> some View {
|
||||
private func serverImage() -> some View {
|
||||
let status = chat.serverInfo.networkStatus
|
||||
return Image(systemName: status.imageName)
|
||||
.foregroundColor(status == .connected ? .green : .secondary)
|
||||
.font(.system(size: 12))
|
||||
}
|
||||
|
||||
func deleteContactButton() -> some View {
|
||||
private func deleteContactButton() -> some View {
|
||||
Button(role: .destructive) {
|
||||
alert = .deleteContactAlert
|
||||
} label: {
|
||||
@@ -237,7 +276,7 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func clearChatButton() -> some View {
|
||||
private func clearChatButton() -> some View {
|
||||
Button() {
|
||||
alert = .clearChatAlert
|
||||
} label: {
|
||||
@@ -323,7 +362,9 @@ struct ChatInfoView_Previews: PreviewProvider {
|
||||
chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []),
|
||||
contact: Contact.sampleData,
|
||||
connectionStats: Binding.constant(nil),
|
||||
localAlias: ""
|
||||
customUserProfile: Binding.constant(nil),
|
||||
localAlias: "",
|
||||
connectionCode: Binding.constant(nil)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ struct ChatView: View {
|
||||
@State private var showDeleteMessage = false
|
||||
@State private var connectionStats: ConnectionStats?
|
||||
@State private var customUserProfile: Profile?
|
||||
@State private var connectionCode: String?
|
||||
@State private var tableView: UITableView?
|
||||
@State private var loadingItems = false
|
||||
@State private var firstPage = false
|
||||
@@ -33,8 +34,7 @@ struct ChatView: View {
|
||||
@FocusState private var searchFocussed
|
||||
// opening GroupMemberInfoView on member icon
|
||||
@State private var selectedMember: GroupMember? = nil
|
||||
@State private var memberConnectionStats: ConnectionStats?
|
||||
|
||||
|
||||
var body: some View {
|
||||
let cInfo = chat.chatInfo
|
||||
return VStack(spacing: 0) {
|
||||
@@ -90,24 +90,30 @@ struct ChatView: View {
|
||||
Button {
|
||||
Task {
|
||||
do {
|
||||
let (stats, profile) = try await apiContactInfo(contactId: chat.chatInfo.apiId)
|
||||
let (stats, profile) = try await apiContactInfo(chat.chatInfo.apiId)
|
||||
let (ct, code) = try await apiGetContactCode(chat.chatInfo.apiId)
|
||||
await MainActor.run {
|
||||
connectionStats = stats
|
||||
customUserProfile = profile
|
||||
connectionCode = code
|
||||
if contact.activeConn.connectionCode != ct.activeConn.connectionCode {
|
||||
chat.chatInfo = .direct(contact: ct)
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiContactInfo error: \(responseError(error))")
|
||||
logger.error("apiContactInfo or apiGetContactCode error: \(responseError(error))")
|
||||
}
|
||||
await MainActor.run { showChatInfoSheet = true }
|
||||
}
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
.appSheet(isPresented: $showChatInfoSheet, onDismiss: {
|
||||
.sheet(isPresented: $showChatInfoSheet, onDismiss: {
|
||||
connectionStats = nil
|
||||
customUserProfile = nil
|
||||
connectionCode = nil
|
||||
}) {
|
||||
ChatInfoView(chat: chat, contact: contact, connectionStats: $connectionStats, customUserProfile: customUserProfile, localAlias: chat.chatInfo.localAlias)
|
||||
ChatInfoView(chat: chat, contact: contact, connectionStats: $connectionStats, customUserProfile: $customUserProfile, localAlias: chat.chatInfo.localAlias, connectionCode: $connectionCode)
|
||||
}
|
||||
} else if case let .group(groupInfo) = cInfo {
|
||||
Button {
|
||||
@@ -381,22 +387,9 @@ struct ChatView: View {
|
||||
if showMember {
|
||||
ProfileImage(imageStr: member.memberProfile.image)
|
||||
.frame(width: memberImageSize, height: memberImageSize)
|
||||
.onTapGesture {
|
||||
Task {
|
||||
do {
|
||||
let stats = try await apiGroupMemberInfo(member.groupId, member.groupMemberId)
|
||||
await MainActor.run { memberConnectionStats = stats }
|
||||
} catch let error {
|
||||
logger.error("apiGroupMemberInfo error: \(responseError(error))")
|
||||
}
|
||||
await MainActor.run { selectedMember = member }
|
||||
}
|
||||
}
|
||||
.appSheet(item: $selectedMember, onDismiss: {
|
||||
selectedMember = nil
|
||||
memberConnectionStats = nil
|
||||
}) { _ in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: $selectedMember, connectionStats: $memberConnectionStats)
|
||||
.onTapGesture { selectedMember = member }
|
||||
.appSheet(item: $selectedMember) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true)
|
||||
}
|
||||
} else {
|
||||
Rectangle().fill(.clear)
|
||||
|
||||
@@ -34,10 +34,24 @@ struct AddGroupMembersView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
let membersToAdd = filterMembersToAdd(chatModel.groupMembers)
|
||||
if creatingGroup {
|
||||
NavigationView {
|
||||
addGroupMembersView()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button ("Skip") { addedMembersCb?(selectedContacts) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addGroupMembersView()
|
||||
}
|
||||
}
|
||||
|
||||
let v = List {
|
||||
private func addGroupMembersView() -> some View {
|
||||
VStack {
|
||||
let membersToAdd = filterMembersToAdd(chatModel.groupMembers)
|
||||
List {
|
||||
ChatInfoToolbar(chat: chat, imageSize: 48)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.listRowBackground(Color.clear)
|
||||
@@ -80,16 +94,6 @@ struct AddGroupMembersView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if creatingGroup {
|
||||
v.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button ("Skip") { addedMembersCb?(selectedContacts) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
v.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.alert(item: $alert) { alert in
|
||||
|
||||
@@ -14,12 +14,13 @@ struct GroupChatInfoView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@ObservedObject var chat: Chat
|
||||
@State var groupInfo: GroupInfo
|
||||
@State var selectedMember: Int64? = nil
|
||||
@ObservedObject private var alertManager = AlertManager.shared
|
||||
@State private var alert: GroupChatInfoViewAlert? = nil
|
||||
@State private var groupLink: String?
|
||||
@State private var showAddMembersSheet: Bool = false
|
||||
@State private var selectedMember: GroupMember? = nil
|
||||
@State private var connectionStats: ConnectionStats?
|
||||
@State private var connectionCode: String?
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
|
||||
enum GroupChatInfoViewAlert: Identifiable {
|
||||
@@ -65,27 +66,22 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
memberView(groupInfo.membership, user: true)
|
||||
ForEach(members) { member in
|
||||
Button {
|
||||
Task {
|
||||
do {
|
||||
let stats = try await apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
|
||||
await MainActor.run { connectionStats = stats }
|
||||
} catch let error {
|
||||
logger.error("apiGroupMemberInfo error: \(responseError(error))")
|
||||
}
|
||||
await MainActor.run { selectedMember = member }
|
||||
}
|
||||
} label: { memberView(member) }
|
||||
NavLinkPlain(
|
||||
tag: member.groupMemberId,
|
||||
selection: $selectedMember,
|
||||
label: { memberView(member) }
|
||||
)
|
||||
}
|
||||
}
|
||||
.appSheet(isPresented: $showAddMembersSheet) {
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
|
||||
}
|
||||
.appSheet(item: $selectedMember, onDismiss: {
|
||||
selectedMember = nil
|
||||
connectionStats = nil
|
||||
}) { _ in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: $selectedMember, connectionStats: $connectionStats)
|
||||
.background(
|
||||
NavigationLink(
|
||||
destination: memberInfoView(selectedMember),
|
||||
isActive: Binding(
|
||||
get: { selectedMember != nil },
|
||||
set: { _, _ in selectedMember = nil }
|
||||
)
|
||||
) { EmptyView() }
|
||||
.opacity(0)
|
||||
)
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -125,7 +121,7 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func groupInfoHeader() -> some View {
|
||||
private func groupInfoHeader() -> some View {
|
||||
VStack {
|
||||
let cInfo = chat.chatInfo
|
||||
ChatInfoImage(chat: chat, color: Color(uiColor: .tertiarySystemFill))
|
||||
@@ -146,35 +142,32 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
|
||||
private func addMembersButton() -> some View {
|
||||
Button {
|
||||
Task {
|
||||
let groupMembers = await apiListMembers(groupInfo.groupId)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.groupMembers = groupMembers
|
||||
showAddMembersSheet = true
|
||||
NavigationLink {
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
|
||||
.onAppear {
|
||||
ChatModel.shared.groupMembers = apiListMembersSync(groupInfo.groupId)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Invite members", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
|
||||
func serverImage() -> some View {
|
||||
private func serverImage() -> some View {
|
||||
let status = chat.serverInfo.networkStatus
|
||||
return Image(systemName: status.imageName)
|
||||
.foregroundColor(status == .connected ? .green : .secondary)
|
||||
}
|
||||
|
||||
func memberView(_ member: GroupMember, user: Bool = false) -> some View {
|
||||
private func memberView(_ member: GroupMember, user: Bool = false) -> some View {
|
||||
HStack{
|
||||
ProfileImage(imageStr: member.image)
|
||||
.frame(width: 38, height: 38)
|
||||
.padding(.trailing, 2)
|
||||
// TODO server connection status
|
||||
VStack(alignment: .leading) {
|
||||
Text(member.chatViewName)
|
||||
let t = Text(member.chatViewName).foregroundColor(member.memberIncognito ? .indigo : .primary)
|
||||
(member.verified ? memberVerifiedShield + t : t)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(member.memberIncognito ? .indigo : .primary)
|
||||
let s = Text(member.memberStatus.shortText)
|
||||
(user ? Text ("you: ") + s : s)
|
||||
.lineLimit(1)
|
||||
@@ -190,6 +183,21 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var memberVerifiedShield: Text {
|
||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
||||
.font(.caption)
|
||||
.baselineOffset(2)
|
||||
.kerning(-2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
@ViewBuilder private func memberInfoView(_ groupMemberId: Int64?) -> some View {
|
||||
if let mId = groupMemberId, let member = chatModel.groupMembers.first(where: { $0.groupMemberId == mId }) {
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member)
|
||||
.navigationBarHidden(false)
|
||||
}
|
||||
}
|
||||
|
||||
private func groupLinkButton() -> some View {
|
||||
NavigationLink {
|
||||
GroupLinkView(groupId: groupInfo.groupId, groupLink: $groupLink)
|
||||
@@ -200,7 +208,7 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func editGroupButton() -> some View {
|
||||
private func editGroupButton() -> some View {
|
||||
NavigationLink {
|
||||
GroupProfileView(
|
||||
groupInfo: $groupInfo,
|
||||
@@ -213,7 +221,7 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func deleteGroupButton() -> some View {
|
||||
private func deleteGroupButton() -> some View {
|
||||
Button(role: .destructive) {
|
||||
alert = .deleteGroupAlert
|
||||
} label: {
|
||||
@@ -222,7 +230,7 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func clearChatButton() -> some View {
|
||||
private func clearChatButton() -> some View {
|
||||
Button() {
|
||||
alert = .clearChatAlert
|
||||
} label: {
|
||||
@@ -231,7 +239,7 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func leaveGroupButton() -> some View {
|
||||
private func leaveGroupButton() -> some View {
|
||||
Button(role: .destructive) {
|
||||
alert = .leaveGroupAlert
|
||||
} label: {
|
||||
|
||||
@@ -13,8 +13,10 @@ struct GroupMemberInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
var groupInfo: GroupInfo
|
||||
@Binding var member: GroupMember?
|
||||
@Binding var connectionStats: ConnectionStats?
|
||||
@State var member: GroupMember
|
||||
var navigation: Bool = false
|
||||
@State private var connectionStats: ConnectionStats? = nil
|
||||
@State private var connectionCode: String? = nil
|
||||
@State private var newRole: GroupMemberRole = .member
|
||||
@State private var alert: GroupMemberInfoViewAlert?
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@@ -36,76 +38,93 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
if let member = member {
|
||||
List {
|
||||
groupMemberInfoHeader(member)
|
||||
.listRowBackground(Color.clear)
|
||||
if navigation {
|
||||
NavigationView { groupMemberInfoView() }
|
||||
} else {
|
||||
groupMemberInfoView()
|
||||
}
|
||||
}
|
||||
|
||||
private func groupMemberInfoView() -> some View {
|
||||
VStack {
|
||||
List {
|
||||
groupMemberInfoHeader(member)
|
||||
.listRowBackground(Color.clear)
|
||||
|
||||
Section {
|
||||
if let contactId = member.memberContactId {
|
||||
if let chat = chatModel.getContactChat(contactId),
|
||||
chat.chatInfo.contact?.directContact ?? false {
|
||||
Section {
|
||||
knownDirectChatButton(chat)
|
||||
}
|
||||
knownDirectChatButton(chat)
|
||||
} else if groupInfo.fullGroupPreferences.directMessages.on {
|
||||
Section {
|
||||
newDirectChatButton(contactId)
|
||||
}
|
||||
newDirectChatButton(contactId)
|
||||
}
|
||||
}
|
||||
if let code = connectionCode { verifyCodeButton(code) }
|
||||
}
|
||||
|
||||
Section("Member") {
|
||||
infoRow("Group", groupInfo.displayName)
|
||||
Section("Member") {
|
||||
infoRow("Group", groupInfo.displayName)
|
||||
|
||||
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
|
||||
Picker("Change role", selection: $newRole) {
|
||||
ForEach(roles) { role in
|
||||
Text(role.text)
|
||||
}
|
||||
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
|
||||
Picker("Change role", selection: $newRole) {
|
||||
ForEach(roles) { role in
|
||||
Text(role.text)
|
||||
}
|
||||
} else {
|
||||
infoRow("Role", member.memberRole.text)
|
||||
}
|
||||
|
||||
// TODO invited by - need to get contact by contact id
|
||||
if let conn = member.activeConn {
|
||||
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
|
||||
infoRow("Connection", connLevelDesc)
|
||||
}
|
||||
.frame(height: 36)
|
||||
} else {
|
||||
infoRow("Role", member.memberRole.text)
|
||||
}
|
||||
|
||||
// TODO invited by - need to get contact by contact id
|
||||
if let conn = member.activeConn {
|
||||
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
|
||||
infoRow("Connection", connLevelDesc)
|
||||
}
|
||||
}
|
||||
|
||||
if let connStats = connectionStats {
|
||||
Section("Servers") {
|
||||
// TODO network connection status
|
||||
Button("Change receiving address") {
|
||||
alert = .switchAddressAlert
|
||||
}
|
||||
if let connStats = connectionStats {
|
||||
smpServers("Receiving via", connStats.rcvServers)
|
||||
smpServers("Sending via", connStats.sndServers)
|
||||
}
|
||||
}
|
||||
|
||||
if member.canBeRemoved(groupInfo: groupInfo) {
|
||||
Section {
|
||||
removeMemberButton(member)
|
||||
}
|
||||
}
|
||||
|
||||
if developerTools {
|
||||
Section("For console") {
|
||||
infoRow("Local name", member.localDisplayName)
|
||||
infoRow("Database ID", "\(member.groupMemberId)")
|
||||
}
|
||||
smpServers("Receiving via", connStats.rcvServers)
|
||||
smpServers("Sending via", connStats.sndServers)
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.onAppear { newRole = member.memberRole }
|
||||
.onChange(of: newRole) { _ in
|
||||
if newRole != member.memberRole {
|
||||
alert = .changeMemberRoleAlert(mem: member, role: newRole)
|
||||
|
||||
if member.canBeRemoved(groupInfo: groupInfo) {
|
||||
Section {
|
||||
removeMemberButton(member)
|
||||
}
|
||||
}
|
||||
|
||||
if developerTools {
|
||||
Section("For console") {
|
||||
infoRow("Local name", member.localDisplayName)
|
||||
infoRow("Database ID", "\(member.groupMemberId)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.onAppear {
|
||||
newRole = member.memberRole
|
||||
do {
|
||||
let stats = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
|
||||
let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
|
||||
member = mem
|
||||
connectionStats = stats
|
||||
connectionCode = code
|
||||
} catch let error {
|
||||
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
.onChange(of: newRole) { _ in
|
||||
if newRole != member.memberRole {
|
||||
alert = .changeMemberRoleAlert(mem: member, role: newRole)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
@@ -155,10 +174,15 @@ struct GroupMemberInfoView: View {
|
||||
.frame(width: 192, height: 192)
|
||||
.padding(.top, 12)
|
||||
.padding()
|
||||
Text(mem.displayName)
|
||||
.font(.largeTitle)
|
||||
.lineLimit(1)
|
||||
.padding(.bottom, 2)
|
||||
HStack {
|
||||
if mem.verified {
|
||||
Image(systemName: "checkmark.shield")
|
||||
}
|
||||
Text(mem.displayName)
|
||||
.font(.largeTitle)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(.bottom, 2)
|
||||
if mem.fullName != "" && mem.fullName != mem.displayName {
|
||||
Text(mem.fullName)
|
||||
.font(.title2)
|
||||
@@ -168,7 +192,38 @@ struct GroupMemberInfoView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
|
||||
func removeMemberButton(_ mem: GroupMember) -> some View {
|
||||
private func verifyCodeButton(_ code: String) -> some View {
|
||||
NavigationLink {
|
||||
VerifyCodeView(
|
||||
displayName: member.displayName,
|
||||
connectionCode: code,
|
||||
connectionVerified: member.verified,
|
||||
verify: { code in
|
||||
if let r = apiVerifyGroupMember(member.groupId, member.groupMemberId, connectionCode: code) {
|
||||
let (verified, existingCode) = r
|
||||
let connCode = verified ? SecurityCode(securityCode: existingCode, verifiedAt: .now) : nil
|
||||
connectionCode = existingCode
|
||||
member.activeConn?.connectionCode = connCode
|
||||
if let i = chatModel.groupMembers.firstIndex(where: { $0.groupMemberId == member.groupMemberId }) {
|
||||
chatModel.groupMembers[i].activeConn?.connectionCode = connCode
|
||||
}
|
||||
return r
|
||||
}
|
||||
return nil
|
||||
}
|
||||
)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Security code")
|
||||
} label: {
|
||||
Label(
|
||||
member.verified ? "View security code" : "Verify security code",
|
||||
systemImage: member.verified ? "checkmark.shield" : "shield"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func removeMemberButton(_ mem: GroupMember) -> some View {
|
||||
Button(role: .destructive) {
|
||||
alert = .removeMemberAlert(mem: mem)
|
||||
} label: {
|
||||
@@ -230,9 +285,7 @@ struct GroupMemberInfoView: View {
|
||||
private func switchMemberAddress() {
|
||||
Task {
|
||||
do {
|
||||
if let member = member {
|
||||
try await apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
}
|
||||
try await apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
} catch let error {
|
||||
logger.error("switchMemberAddress apiSwitchGroupMember error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error changing address")
|
||||
@@ -248,8 +301,7 @@ struct GroupMemberInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GroupMemberInfoView(
|
||||
groupInfo: GroupInfo.sampleData,
|
||||
member: Binding.constant(GroupMember.sampleData),
|
||||
connectionStats: Binding.constant(nil)
|
||||
member: GroupMember.sampleData
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
60
apps/ios/Shared/Views/Chat/ScanCodeView.swift
Normal file
60
apps/ios/Shared/Views/Chat/ScanCodeView.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// ScanCodeView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 10/12/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CodeScanner
|
||||
|
||||
struct ScanCodeView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@Binding var connectionVerified: Bool
|
||||
var verify: (String?) async -> (Bool, String)?
|
||||
@State private var showCodeError = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
CodeScannerView(codeTypes: [.qr], completion: processQRCode)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.border(.gray)
|
||||
Text("Scan security code from your contact's app.")
|
||||
.padding(.top)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.alert(isPresented: $showCodeError) {
|
||||
Alert(title: Text("Incorrect security code!"))
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
|
||||
func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
||||
switch resp {
|
||||
case let .success(r):
|
||||
Task {
|
||||
if let (ok, _) = await verify(r.string) {
|
||||
await MainActor.run {
|
||||
connectionVerified = ok
|
||||
if ok {
|
||||
dismiss()
|
||||
} else {
|
||||
showCodeError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .failure(e):
|
||||
logger.error("ScanCodeView.processQRCode QR code error: \(e.localizedDescription)")
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ScanCodeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ScanCodeView(connectionVerified: Binding.constant(true), verify: {_ in nil})
|
||||
}
|
||||
}
|
||||
126
apps/ios/Shared/Views/Chat/VerifyCodeView.swift
Normal file
126
apps/ios/Shared/Views/Chat/VerifyCodeView.swift
Normal file
@@ -0,0 +1,126 @@
|
||||
//
|
||||
// VerifyCodeView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 10/12/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct VerifyCodeView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
var displayName: String
|
||||
@State var connectionCode: String?
|
||||
@State var connectionVerified: Bool
|
||||
var verify: (String?) -> (Bool, String)?
|
||||
@State private var showCodeError = false
|
||||
|
||||
var body: some View {
|
||||
if let code = connectionCode {
|
||||
verifyCodeView(code)
|
||||
}
|
||||
}
|
||||
|
||||
private func verifyCodeView(_ code: String) -> some View {
|
||||
ScrollView {
|
||||
let splitCode = splitToParts(code, length: 24)
|
||||
VStack(alignment: .leading) {
|
||||
Group {
|
||||
HStack {
|
||||
if connectionVerified {
|
||||
Image(systemName: "checkmark.shield")
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(displayName) is verified")
|
||||
} else {
|
||||
Text("\(displayName) is not verified")
|
||||
}
|
||||
}
|
||||
.frame(height: 24)
|
||||
|
||||
QRCode(uri: code)
|
||||
.padding(.horizontal)
|
||||
|
||||
Text(splitCode)
|
||||
.multilineTextAlignment(.leading)
|
||||
.font(.body.monospaced())
|
||||
.lineLimit(20)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
Text("To verify end-to-end encryption with your contact compare (or scan) the code on your devices.")
|
||||
.padding(.bottom)
|
||||
|
||||
Group {
|
||||
if connectionVerified {
|
||||
Button {
|
||||
verifyCode(nil)
|
||||
} label: {
|
||||
Label("Clear verification", systemImage: "shield")
|
||||
}
|
||||
.padding()
|
||||
} else {
|
||||
HStack {
|
||||
NavigationLink {
|
||||
ScanCodeView(connectionVerified: $connectionVerified, verify: verify)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Scan code")
|
||||
} label: {
|
||||
Label("Scan code", systemImage: "qrcode")
|
||||
}
|
||||
.padding()
|
||||
Button {
|
||||
verifyCode(code) { verified in
|
||||
if !verified { showCodeError = true }
|
||||
}
|
||||
} label: {
|
||||
Label("Mark verified", systemImage: "checkmark.shield")
|
||||
}
|
||||
.padding()
|
||||
.alert(isPresented: $showCodeError) {
|
||||
Alert(title: Text("Incorrect security code!"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
showShareSheet(items: [splitCode])
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: connectionVerified) { _ in
|
||||
if connectionVerified { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func verifyCode(_ code: String?, _ cb: ((Bool) -> Void)? = nil) {
|
||||
if let (verified, existingCode) = verify(code) {
|
||||
connectionVerified = verified
|
||||
connectionCode = existingCode
|
||||
cb?(verified)
|
||||
}
|
||||
}
|
||||
|
||||
private func splitToParts(_ s: String, length: Int) -> String {
|
||||
if length >= s.count { return s }
|
||||
return (0 ... (s.count - 1) / length)
|
||||
.map { s.dropFirst($0 * length).prefix(length) }
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
struct VerifyCodeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VerifyCodeView(displayName: "alice", connectionCode: "12345 67890 12345 67890", connectionVerified: false, verify: {_ in nil})
|
||||
}
|
||||
}
|
||||
@@ -79,26 +79,33 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatPreviewTitle() -> some View {
|
||||
let v = Text(chat.chatInfo.chatViewName)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.lineLimit(1)
|
||||
.frame(alignment: .topLeading)
|
||||
switch (chat.chatInfo) {
|
||||
case .direct:
|
||||
v.foregroundColor(chat.chatInfo.ready ? .primary : .secondary)
|
||||
case .group(groupInfo: let groupInfo):
|
||||
let t = Text(chat.chatInfo.chatViewName).font(.title3).fontWeight(.bold)
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact):
|
||||
previewTitle(contact.verified == true ? verifiedIcon + t : t)
|
||||
.foregroundColor(chat.chatInfo.ready ? .primary : .secondary)
|
||||
case let .group(groupInfo):
|
||||
let v = previewTitle(t)
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
case .memInvited:
|
||||
chat.chatInfo.incognito ? v.foregroundColor(.indigo) : v.foregroundColor(.accentColor)
|
||||
case .memAccepted:
|
||||
v.foregroundColor(.secondary)
|
||||
case .memInvited: v.foregroundColor(chat.chatInfo.incognito ? .indigo : .accentColor)
|
||||
case .memAccepted: v.foregroundColor(.secondary)
|
||||
default: v
|
||||
}
|
||||
default: v
|
||||
default: previewTitle(t)
|
||||
}
|
||||
}
|
||||
|
||||
private func previewTitle(_ t: Text) -> some View {
|
||||
t.lineLimit(1).frame(alignment: .topLeading)
|
||||
}
|
||||
|
||||
private var verifiedIcon: Text {
|
||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
||||
.foregroundColor(.secondary)
|
||||
.baselineOffset(1)
|
||||
.kerning(-2)
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatPreviewText(_ cItem: ChatItem?) -> some View {
|
||||
if let cItem = cItem {
|
||||
let itemText = !cItem.meta.itemDeleted ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
|
||||
|
||||
@@ -47,7 +47,7 @@ struct ScanSMPServer: View {
|
||||
showAddressError = true
|
||||
}
|
||||
case let .failure(e):
|
||||
logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)")
|
||||
logger.error("ScanSMPServer.processQRCode QR code error: \(e.localizedDescription)")
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,8 @@
|
||||
5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; };
|
||||
5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E327A8683A00ACCCDD /* UserAddress.swift */; };
|
||||
5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */; };
|
||||
5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */; };
|
||||
5CBE6C142944CC12002D9531 /* ScanCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBE6C132944CC12002D9531 /* ScanCodeView.swift */; };
|
||||
5CC1C99227A6C7F5000D9FF6 /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */; };
|
||||
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */; };
|
||||
5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; };
|
||||
@@ -306,6 +308,8 @@
|
||||
5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = "<group>"; };
|
||||
5CB924E327A8683A00ACCCDD /* UserAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddress.swift; sourceTree = "<group>"; };
|
||||
5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListNavLink.swift; sourceTree = "<group>"; };
|
||||
5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyCodeView.swift; sourceTree = "<group>"; };
|
||||
5CBE6C132944CC12002D9531 /* ScanCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanCodeView.swift; sourceTree = "<group>"; };
|
||||
5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCode.swift; sourceTree = "<group>"; };
|
||||
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = "<group>"; };
|
||||
5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@@ -453,6 +457,8 @@
|
||||
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */,
|
||||
5CE4407127ADB1D0007B033A /* Emoji.swift */,
|
||||
5CADE79B292131E900072E13 /* ContactPreferencesView.swift */,
|
||||
5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */,
|
||||
5CBE6C132944CC12002D9531 /* ScanCodeView.swift */,
|
||||
);
|
||||
path = Chat;
|
||||
sourceTree = "<group>";
|
||||
@@ -936,6 +942,7 @@
|
||||
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */,
|
||||
5C029EAA283942EA004A9677 /* CallController.swift in Sources */,
|
||||
5CCA7DF32905735700C8FEBA /* AcceptRequestsView.swift in Sources */,
|
||||
5CBE6C142944CC12002D9531 /* ScanCodeView.swift in Sources */,
|
||||
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */,
|
||||
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */,
|
||||
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */,
|
||||
@@ -943,6 +950,7 @@
|
||||
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */,
|
||||
5CB2084F28DA4B4800D024EC /* RTCServers.swift in Sources */,
|
||||
5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */,
|
||||
5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */,
|
||||
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */,
|
||||
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */,
|
||||
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */,
|
||||
|
||||
@@ -58,6 +58,10 @@ public enum ChatCommand {
|
||||
case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64)
|
||||
case apiSwitchContact(contactId: Int64)
|
||||
case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64)
|
||||
case apiGetContactCode(contactId: Int64)
|
||||
case apiGetGroupMemberCode(groupId: Int64, groupMemberId: Int64)
|
||||
case apiVerifyContact(contactId: Int64, connectionCode: String?)
|
||||
case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?)
|
||||
case addContact
|
||||
case connect(connReq: String)
|
||||
case apiDeleteChat(type: ChatType, id: Int64)
|
||||
@@ -138,6 +142,12 @@ public enum ChatCommand {
|
||||
case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)"
|
||||
case let .apiSwitchContact(contactId): return "/_switch @\(contactId)"
|
||||
case let .apiSwitchGroupMember(groupId, groupMemberId): return "/_switch #\(groupId) \(groupMemberId)"
|
||||
case let .apiGetContactCode(contactId): return "/_get code @\(contactId)"
|
||||
case let .apiGetGroupMemberCode(groupId, groupMemberId): return "/_get code #\(groupId) \(groupMemberId)"
|
||||
case let .apiVerifyContact(contactId, .some(connectionCode)): return "/_verify code @\(contactId) \(connectionCode)"
|
||||
case let .apiVerifyContact(contactId, .none): return "/_verify code @\(contactId)"
|
||||
case let .apiVerifyGroupMember(groupId, groupMemberId, .some(connectionCode)): return "/_verify code #\(groupId) \(groupMemberId) \(connectionCode)"
|
||||
case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)"
|
||||
case .addContact: return "/connect"
|
||||
case let .connect(connReq): return "/connect \(connReq)"
|
||||
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
|
||||
@@ -217,6 +227,10 @@ public enum ChatCommand {
|
||||
case .apiGroupMemberInfo: return "apiGroupMemberInfo"
|
||||
case .apiSwitchContact: return "apiSwitchContact"
|
||||
case .apiSwitchGroupMember: return "apiSwitchGroupMember"
|
||||
case .apiGetContactCode: return "apiGetContactCode"
|
||||
case .apiGetGroupMemberCode: return "apiGetGroupMemberCode"
|
||||
case .apiVerifyContact: return "apiVerifyContact"
|
||||
case .apiVerifyGroupMember: return "apiVerifyGroupMember"
|
||||
case .addContact: return "addContact"
|
||||
case .connect: return "connect"
|
||||
case .apiDeleteChat: return "apiDeleteChat"
|
||||
@@ -300,6 +314,9 @@ public enum ChatResponse: Decodable, Error {
|
||||
case networkConfig(networkConfig: NetCfg)
|
||||
case contactInfo(contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?)
|
||||
case groupMemberInfo(groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?)
|
||||
case contactCode(contact: Contact, connectionCode: String)
|
||||
case groupMemberCode(groupInfo: GroupInfo, member: GroupMember, connectionCode: String)
|
||||
case connectionVerified(verified: Bool, expectedCode: String)
|
||||
case invitation(connReqInvitation: String)
|
||||
case sentConfirmation
|
||||
case sentInvitation
|
||||
@@ -403,6 +420,9 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .networkConfig: return "networkConfig"
|
||||
case .contactInfo: return "contactInfo"
|
||||
case .groupMemberInfo: return "groupMemberInfo"
|
||||
case .contactCode: return "contactCode"
|
||||
case .groupMemberCode: return "groupMemberCode"
|
||||
case .connectionVerified: return "connectionVerified"
|
||||
case .invitation: return "invitation"
|
||||
case .sentConfirmation: return "sentConfirmation"
|
||||
case .sentInvitation: return "sentInvitation"
|
||||
@@ -506,6 +526,9 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .networkConfig(networkConfig): return String(describing: networkConfig)
|
||||
case let .contactInfo(contact, connectionStats, customUserProfile): return "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))"
|
||||
case let .groupMemberInfo(groupInfo, member, connectionStats_): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_)))"
|
||||
case let .contactCode(contact, connectionCode): return "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)"
|
||||
case let .groupMemberCode(groupInfo, member, connectionCode): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)"
|
||||
case let .connectionVerified(verified, expectedCode): return "verified: \(verified)\nconnectionCode: \(expectedCode)"
|
||||
case let .invitation(connReqInvitation): return connReqInvitation
|
||||
case .sentConfirmation: return noDetails
|
||||
case .sentInvitation: return noDetails
|
||||
|
||||
@@ -817,6 +817,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
||||
public var fullName: String { get { profile.fullName } }
|
||||
public var image: String? { get { profile.image } }
|
||||
public var localAlias: String { profile.localAlias }
|
||||
public var verified: Bool { activeConn.connectionCode != nil }
|
||||
|
||||
public var directContact: Bool {
|
||||
(activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed
|
||||
@@ -858,6 +859,7 @@ public struct Connection: Decodable {
|
||||
public var connLevel: Int
|
||||
public var viaGroupLink: Bool
|
||||
public var customUserProfileId: Int64?
|
||||
public var connectionCode: SecurityCode?
|
||||
|
||||
public var id: ChatId { get { ":\(connId)" } }
|
||||
|
||||
@@ -869,6 +871,16 @@ public struct Connection: Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
public struct SecurityCode: Decodable, Equatable {
|
||||
public init(securityCode: String, verifiedAt: Date) {
|
||||
self.securityCode = securityCode
|
||||
self.verifiedAt = verifiedAt
|
||||
}
|
||||
|
||||
public var securityCode: String
|
||||
public var verifiedAt: Date
|
||||
}
|
||||
|
||||
public struct UserContact: Decodable {
|
||||
public var userContactLinkId: Int64
|
||||
|
||||
@@ -1124,6 +1136,7 @@ public struct GroupMember: Identifiable, Decodable {
|
||||
}
|
||||
public var fullName: String { get { memberProfile.fullName } }
|
||||
public var image: String? { get { memberProfile.image } }
|
||||
public var verified: Bool { activeConn?.connectionCode != nil }
|
||||
|
||||
var directChatId: ChatId? {
|
||||
get {
|
||||
|
||||
Reference in New Issue
Block a user