diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt
index 8013beab0c..bbc4e021b5 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt
@@ -479,6 +479,11 @@ class Profile(
override val fullName: String,
override val image: String? = null
): NamedChat {
+ val displayNameWithOptionalFullName: String
+ get() {
+ return if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)"
+ }
+
companion object {
val sampleData = Profile(
displayName = "alice",
@@ -857,6 +862,15 @@ data class ChatItem (
quotedItem = null,
file = null
)
+
+ fun getGroupEventSample() =
+ ChatItem(
+ chatDir = CIDirection.DirectRcv(),
+ meta = CIMeta.getSample(1, Clock.System.now(), "group event text", CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false),
+ content = CIContent.RcvGroupEventContent(rcvGroupEvent = RcvGroupEvent.MemberAdded(groupMemberId = 1, profile = Profile.sampleData)),
+ quotedItem = null,
+ file = null
+ )
}
}
@@ -949,6 +963,8 @@ sealed class CIContent: ItemContent {
@Serializable @SerialName("rcvIntegrityError") class RcvIntegrityError(val msgError: MsgErrorType): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupInvitation") class RcvGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
+ @Serializable @SerialName("rcvGroupEvent") class RcvGroupEventContent(val rcvGroupEvent: RcvGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
+ @Serializable @SerialName("sndGroupEvent") class SndGroupEventContent(val sndGroupEvent: SndGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
override val text: String get() = when(this) {
is SndMsgContent -> msgContent.text
@@ -958,8 +974,10 @@ sealed class CIContent: ItemContent {
is SndCall -> status.text(duration)
is RcvCall -> status.text(duration)
is RcvIntegrityError -> msgError.text
- is RcvGroupInvitation -> groupInvitation.text()
- is SndGroupInvitation -> groupInvitation.text()
+ is RcvGroupInvitation -> groupInvitation.text
+ is SndGroupInvitation -> groupInvitation.text
+ is RcvGroupEventContent -> rcvGroupEvent.text
+ is SndGroupEventContent -> sndGroupEvent.text
}
}
@@ -1061,7 +1079,7 @@ class CIGroupInvitation (
val groupProfile: GroupProfile,
val status: CIGroupInvitationStatus
) {
- fun text(): String = String.format(generalGetString(R.string.group_invitation_item_description), groupProfile.displayName)
+ val text: String get() = String.format(generalGetString(R.string.group_invitation_item_description), groupProfile.displayName)
companion object {
fun getSample(
@@ -1268,3 +1286,33 @@ sealed class MsgErrorType() {
is MsgDuplicate -> generalGetString(R.string.integrity_msg_duplicate) // not used now
}
}
+
+@Serializable
+sealed class RcvGroupEvent() {
+ @Serializable @SerialName("memberAdded") class MemberAdded(val groupMemberId: Long, val profile: Profile): RcvGroupEvent()
+ @Serializable @SerialName("memberConnected") class MemberConnected(): RcvGroupEvent()
+ @Serializable @SerialName("memberLeft") class MemberLeft(): RcvGroupEvent()
+ @Serializable @SerialName("memberDeleted") class MemberDeleted(val groupMemberId: Long, val profile: Profile): RcvGroupEvent()
+ @Serializable @SerialName("userDeleted") class UserDeleted(): RcvGroupEvent()
+ @Serializable @SerialName("groupDeleted") class GroupDeleted(): RcvGroupEvent()
+
+ val text: String get() = when (this) {
+ is MemberAdded -> String.format(generalGetString(R.string.rcv_group_event_member_added), profile.displayNameWithOptionalFullName)
+ is MemberConnected -> generalGetString(R.string.rcv_group_event_member_connected)
+ is MemberLeft -> generalGetString(R.string.rcv_group_event_member_left)
+ is MemberDeleted -> String.format(generalGetString(R.string.rcv_group_event_member_deleted), profile.displayNameWithOptionalFullName)
+ is UserDeleted -> generalGetString(R.string.rcv_group_event_user_deleted)
+ is GroupDeleted -> generalGetString(R.string.rcv_group_event_group_deleted)
+ }
+}
+
+@Serializable
+sealed class SndGroupEvent() {
+ @Serializable @SerialName("memberDeleted") class MemberDeleted(val groupMemberId: Long, val profile: Profile): SndGroupEvent()
+ @Serializable @SerialName("userLeft") class UserLeft(): SndGroupEvent()
+
+ val text: String get() = when (this) {
+ is MemberDeleted -> String.format(generalGetString(R.string.snd_group_event_member_deleted), profile.displayNameWithOptionalFullName)
+ is UserLeft -> generalGetString(R.string.snd_group_event_user_left)
+ }
+}
diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt
index fda4d6949b..7297eeec45 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt
@@ -13,6 +13,7 @@ val Inter = FontFamily(
Font(R.font.inter_bold, weight = FontWeight.Bold),
Font(R.font.inter_semi_bold, weight = FontWeight.SemiBold),
Font(R.font.inter_medium, weight = FontWeight.Medium),
+ Font(R.font.inter_light, weight = FontWeight.Light),
)
// Set of Material typography styles to start with
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIGroupEventView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIGroupEventView.kt
new file mode 100644
index 0000000000..1915c97746
--- /dev/null
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIGroupEventView.kt
@@ -0,0 +1,59 @@
+package chat.simplex.app.views.chat.item
+
+import android.content.res.Configuration
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.*
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import chat.simplex.app.model.ChatItem
+import chat.simplex.app.ui.theme.HighOrLowlight
+import chat.simplex.app.ui.theme.SimpleXTheme
+
+@Composable
+fun CIGroupEventView(ci: ChatItem) {
+ fun withGroupEventStyle(builder: AnnotatedString.Builder, text: String) {
+ return builder.withStyle(SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight)) { append(text) }
+ }
+
+ Surface {
+ Row(
+ Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
+ verticalAlignment = Alignment.Bottom
+ ) {
+ Text(
+ buildAnnotatedString {
+ val memberDisplayName = ci.memberDisplayName
+ if (memberDisplayName != null) {
+ withGroupEventStyle(this, memberDisplayName)
+ append(" ")
+ }
+ withGroupEventStyle(this, ci.content.text)
+ append(" ")
+ withGroupEventStyle(this, ci.timestampText)
+ },
+ style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Preview(
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ name = "Dark Mode"
+)
+@Composable
+fun CIGroupEventViewPreview() {
+ SimpleXTheme {
+ CIGroupEventView(
+ ChatItem.getGroupEventSample()
+ )
+ }
+}
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt
index b98560a839..a343bb6cf8 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt
@@ -149,6 +149,8 @@ fun ChatItemView(
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, showMember = showMember)
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup)
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup)
+ is CIContent.RcvGroupEventContent -> CIGroupEventView(cItem)
+ is CIContent.SndGroupEventContent -> CIGroupEventView(cItem)
}
}
}
diff --git a/apps/android/app/src/main/res/font/inter_light.ttf b/apps/android/app/src/main/res/font/inter_light.ttf
new file mode 100644
index 0000000000..ebaa005740
Binary files /dev/null and b/apps/android/app/src/main/res/font/inter_light.ttf differ
diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml
index 24b17d38ad..bfcd7606f5 100644
--- a/apps/android/app/src/main/res/values-ru/strings.xml
+++ b/apps/android/app/src/main/res/values-ru/strings.xml
@@ -420,7 +420,7 @@
Принять звонок
- %1$d пропущенных сообщений"
+ %1$d пропущенных сообщений
ошибка хэш сообщения
ошибка ID сообщения
повторное сообщение
@@ -505,4 +505,14 @@
Вы вступили в эту группу
Вы отклонили приглашение в группу
Приглашение в группу истекло
+
+
+ пригласил(а) %1$s
+ соединен(а)
+ покинул(а) группу
+ удалил(а) %1$s
+ удалил(а) вас из группы
+ удалил(а) группу
+ вы удалили %1$s
+ вы покинули группу
diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml
index 5d6f51a6a1..ef6d0abcac 100644
--- a/apps/android/app/src/main/res/values/strings.xml
+++ b/apps/android/app/src/main/res/values/strings.xml
@@ -422,7 +422,7 @@
Answer call
- %1$d skipped message(s)"
+ %1$d skipped message(s)
bad message hash
bad message ID
duplicate message
@@ -507,4 +507,14 @@
You joined this group
You rejected group invitation
Group invitation expired
+
+
+ invited %1$s
+ connected
+ left
+ removed %1$s
+ removed you
+ deleted group
+ you removed %1$s
+ you left
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupEventView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupEventView.swift
new file mode 100644
index 0000000000..4c5579653c
--- /dev/null
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupEventView.swift
@@ -0,0 +1,50 @@
+//
+// CIGroupEventView.swift
+// SimpleX (iOS)
+//
+// Created by JRoberts on 20.07.2022.
+// Copyright © 2022 SimpleX Chat. All rights reserved.
+//
+
+import SwiftUI
+import SimpleXChat
+
+struct CIGroupEventView: View {
+ var chatItem: ChatItem
+
+ var body: some View {
+ HStack(alignment: .bottom, spacing: 0) {
+ if let member = chatItem.memberDisplayName {
+ Text(member)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ .fontWeight(.light)
+ + Text(" ")
+ + eventText()
+ } else {
+ eventText()
+ }
+ }
+ .padding(.leading, 6)
+ .padding(.bottom, 6)
+ .textSelection(.disabled)
+ }
+
+ func eventText() -> Text {
+ Text(chatItem.content.text)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ .fontWeight(.light)
+ + Text(" ")
+ + chatItem.timestampText
+ .font(.caption)
+ .foregroundColor(Color.secondary)
+ .fontWeight(.light)
+ }
+}
+
+struct CIGroupEventView_Previews: PreviewProvider {
+ static var previews: some View {
+ CIGroupEventView(chatItem: ChatItem.getGroupEventSample())
+ }
+}
diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift
index ce5f68dc2b..c43fc9daf0 100644
--- a/apps/ios/Shared/Views/Chat/ChatItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift
@@ -26,6 +26,8 @@ struct ChatItemView: View {
case .rcvIntegrityError: IntegrityErrorItemView(chatItem: chatItem, showMember: showMember)
case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
+ case .rcvGroupEvent: groupEventItemView()
+ case .sndGroupEvent: groupEventItemView()
}
}
@@ -48,6 +50,10 @@ struct ChatItemView: View {
private func groupInvitationItemView(_ groupInvitation: CIGroupInvitation, _ memberRole: GroupMemberRole) -> some View {
CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole)
}
+
+ private func groupEventItemView() -> some View {
+ CIGroupEventView(chatItem: chatItem)
+ }
}
struct ChatItemView_Previews: PreviewProvider {
diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj
index 2dae8a4b2c..c865b201ce 100644
--- a/apps/ios/SimpleX.xcodeproj/project.pbxproj
+++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj
@@ -111,6 +111,7 @@
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; };
+ 6440CA00288857A10062C672 /* CIGroupEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIGroupEventView.swift */; };
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; };
6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */; };
6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; };
@@ -286,6 +287,7 @@
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = ""; };
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = ""; };
+ 6440C9FF288857A10062C672 /* CIGroupEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupEventView.swift; sourceTree = ""; };
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = ""; };
6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = ""; };
6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = ""; };
@@ -592,6 +594,7 @@
5C029EA72837DBB3004A9677 /* CICallItemView.swift */,
5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */,
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */,
+ 6440C9FF288857A10062C672 /* CIGroupEventView.swift */,
);
path = ChatItem;
sourceTree = "";
@@ -880,6 +883,7 @@
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */,
5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */,
3C714777281C081000CB4D4B /* WebRTCView.swift in Sources */,
+ 6440CA00288857A10062C672 /* CIGroupEventView.swift in Sources */,
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift
index 59abdad2b6..bfdc2b6a62 100644
--- a/apps/ios/SimpleXChat/ChatTypes.swift
+++ b/apps/ios/SimpleXChat/ChatTypes.swift
@@ -44,6 +44,10 @@ public struct Profile: Codable, NamedChat {
public var fullName: String
public var image: String?
+ var displayNameWithOptionalFullName: String {
+ (fullName == "" || displayName == fullName) ? displayName : "\(displayName) (\(fullName))"
+ }
+
static let sampleData = Profile(
displayName: "alice",
fullName: "Alice"
@@ -680,6 +684,16 @@ public struct ChatItem: Identifiable, Decodable {
file: nil
)
}
+
+ public static func getGroupEventSample () -> ChatItem {
+ ChatItem(
+ chatDir: .directRcv,
+ meta: CIMeta.getSample(1, .now, "group event text", .rcvRead, false, false, false),
+ content: .rcvGroupEvent(rcvGroupEvent: .memberAdded(groupMemberId: 1, profile: Profile.sampleData)),
+ quotedItem: nil,
+ file: nil
+ )
+ }
}
public enum CIDirection: Decodable {
@@ -764,6 +778,8 @@ public enum CIContent: Decodable, ItemContent {
case rcvIntegrityError(msgError: MsgErrorType)
case rcvGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole)
case sndGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole)
+ case rcvGroupEvent(rcvGroupEvent: RcvGroupEvent)
+ case sndGroupEvent(sndGroupEvent: SndGroupEvent)
public var text: String {
get {
@@ -775,8 +791,10 @@ public enum CIContent: Decodable, ItemContent {
case let .sndCall(status, duration): return status.text(duration)
case let .rcvCall(status, duration): return status.text(duration)
case let .rcvIntegrityError(msgError): return msgError.text
- case let .rcvGroupInvitation(groupInvitation, _): return groupInvitation.text()
- case let .sndGroupInvitation(groupInvitation, _): return groupInvitation.text()
+ case let .rcvGroupInvitation(groupInvitation, _): return groupInvitation.text
+ case let .sndGroupInvitation(groupInvitation, _): return groupInvitation.text
+ case let .rcvGroupEvent(rcvGroupEvent): return rcvGroupEvent.text
+ case let .sndGroupEvent(sndGroupEvent): return sndGroupEvent.text
}
}
}
@@ -1095,7 +1113,7 @@ public struct CIGroupInvitation: Decodable {
public var groupProfile: GroupProfile
public var status: CIGroupInvitationStatus
- func text() -> String {
+ var text: String {
String.localizedStringWithFormat(NSLocalizedString("invitation to group %@", comment: "group name"), groupProfile.displayName)
}
@@ -1110,3 +1128,38 @@ public enum CIGroupInvitationStatus: String, Decodable {
case rejected
case expired
}
+
+public enum RcvGroupEvent: Decodable {
+ case memberAdded(groupMemberId: Int64, profile: Profile)
+ case memberConnected
+ case memberLeft
+ case memberDeleted(groupMemberId: Int64, profile: Profile)
+ case userDeleted
+ case groupDeleted
+
+ var text: String {
+ switch self {
+ case let .memberAdded(_, profile):
+ return String.localizedStringWithFormat(NSLocalizedString("invited %@", comment: "rcv group event chat item"), profile.displayNameWithOptionalFullName)
+ case .memberConnected: return NSLocalizedString("connected", comment: "rcv group event chat item")
+ case .memberLeft: return NSLocalizedString("left", comment: "rcv group event chat item")
+ case let .memberDeleted(_, profile):
+ return String.localizedStringWithFormat(NSLocalizedString("removed %@", comment: "rcv group event chat item"), profile.displayNameWithOptionalFullName)
+ case .userDeleted: return NSLocalizedString("removed you", comment: "rcv group event chat item")
+ case .groupDeleted: return NSLocalizedString("deleted group", comment: "rcv group event chat item")
+ }
+ }
+}
+
+public enum SndGroupEvent: Decodable {
+ case memberDeleted(groupMemberId: Int64, profile: Profile)
+ case userLeft
+
+ var text: String {
+ switch self {
+ case let .memberDeleted(_, profile):
+ return String.localizedStringWithFormat(NSLocalizedString("you removed %@", comment: "snd group event chat item"), profile.displayNameWithOptionalFullName)
+ case .userLeft: return NSLocalizedString("you left", comment: "snd group event chat item")
+ }
+ }
+}