android, desktop: group knocking (#5816)

This commit is contained in:
spaced4ndy authored and GitHub committed 2025-04-21 15:17:21 +00:00
1 parent 3095b980cb
commit 805a69da94
34 files changed
+1844 -748

No files matched your search

@@ -55,6 +55,7 @@ actual fun PlatformTextField(
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
userIsPending: Boolean,
placeholder: String,
showVoiceButton: Boolean,
onMessageChange: (ComposeMessage) -> Unit,
@@ -198,6 +199,8 @@ actual fun PlatformTextField(
}
if (composeState.value.preview is ComposePreview.VoicePreview) {
ComposeOverlay(MR.strings.voice_message_send_text, textStyle, padding)
} else if (userIsPending) {
ComposeOverlay(MR.strings.reviewed_by_moderators, textStyle, padding)
} else if (userIsObserver) {
ComposeOverlay(MR.strings.you_are_observer, textStyle, padding)
}
@@ -294,7 +294,7 @@ object ChatModel {
}
}
class ChatsContext(val contentTag: MsgContentTag?) {
class ChatsContext(val secondaryContextFilter: SecondaryContextFilter?) {
val chats = mutableStateOf(SnapshotStateList<Chat>())
/** if you modify the items by adding/removing them, use helpers methods like [addToChatItems], [removeLastChatItems], [removeAllAndNotify], [clearAndNotify] and so on.
* If some helper is missing, create it. Notify is needed to track state of items that we added manually (not via api call). See [apiLoadMessages].
@@ -309,6 +309,20 @@ object ChatModel {
fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id }
private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId }
val contentTag: MsgContentTag? =
when (secondaryContextFilter) {
null -> null
is SecondaryContextFilter.GroupChatScopeContext -> null
is SecondaryContextFilter.MsgContentTagContext -> secondaryContextFilter.contentTag
}
val groupScopeInfo: GroupChatScopeInfo? =
when (secondaryContextFilter) {
null -> null
is SecondaryContextFilter.GroupChatScopeContext -> secondaryContextFilter.groupScopeInfo
is SecondaryContextFilter.MsgContentTagContext -> null
}
suspend fun addChat(chat: Chat) {
chats.add(index = 0, chat)
popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = 0)
@@ -363,7 +377,7 @@ object ChatModel {
updateContact(rhId, updatedContact)
}
suspend fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo))
suspend fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo, groupChatScope = null)) // TODO [knocking] review
private suspend fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) {
if (hasChat(rhId, cInfo.id)) {
@@ -415,55 +429,58 @@ object ChatModel {
}
suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
// mark chat non deleted
if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) {
val updatedContact = cInfo.contact.copy(chatDeleted = false)
updateContact(rhId, updatedContact)
}
// update previews
val i = getChatIndex(rhId, cInfo.id)
val chat: Chat
if (i >= 0) {
chat = chats[i]
val newPreviewItem = when (cInfo) {
is ChatInfo.Group -> {
val currentPreviewItem = chat.chatItems.firstOrNull()
if (currentPreviewItem != null) {
if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) {
cItem
// update chat list
if (cInfo.groupChatScope() == null) {
// mark chat non deleted
if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) {
val updatedContact = cInfo.contact.copy(chatDeleted = false)
updateContact(rhId, updatedContact)
}
// update preview
val i = getChatIndex(rhId, cInfo.id)
val chat: Chat
if (i >= 0) {
chat = chats[i]
val newPreviewItem = when (cInfo) {
is ChatInfo.Group -> {
val currentPreviewItem = chat.chatItems.firstOrNull()
if (currentPreviewItem != null) {
if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) {
cItem
} else {
currentPreviewItem
}
} else {
currentPreviewItem
cItem
}
} else {
cItem
}
}
else -> cItem
}
val wasUnread = chat.unreadTag
chats[i] = chat.copy(
chatItems = arrayListOf(newPreviewItem),
chatStats =
if (cItem.meta.itemStatus is CIStatus.RcvNew) {
increaseUnreadCounter(rhId, currentUser.value!!)
chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, unreadMentions = if (cItem.meta.userMention) chat.chatStats.unreadMentions + 1 else chat.chatStats.unreadMentions)
}
else
chat.chatStats
)
updateChatTagReadNoContentTag(chats[i], wasUnread)
if (appPlatform.isDesktop && cItem.chatDir.sent) {
reorderChat(chats[i], 0)
else -> cItem
}
val wasUnread = chat.unreadTag
chats[i] = chat.copy(
chatItems = arrayListOf(newPreviewItem),
chatStats =
if (cItem.meta.itemStatus is CIStatus.RcvNew) {
increaseUnreadCounter(rhId, currentUser.value!!)
chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, unreadMentions = if (cItem.meta.userMention) chat.chatStats.unreadMentions + 1 else chat.chatStats.unreadMentions)
} else
chat.chatStats
)
updateChatTagReadInPrimaryContext(chats[i], wasUnread)
if (appPlatform.isDesktop && cItem.chatDir.sent) {
reorderChat(chats[i], 0)
} else {
popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = i)
}
} else {
popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = i)
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
}
} else {
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
}
// add to current scope
withContext(Dispatchers.Main) {
// add to current chat
if (chatId.value == cInfo.id) {
if (chatItemBelongsToScope(cInfo, cItem)) {
// Prevent situation when chat item already in the list received from backend
if (chatItems.value.none { it.id == cItem.id }) {
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
@@ -475,30 +492,47 @@ object ChatModel {
}
}
}
suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean {
// update previews
val i = getChatIndex(rhId, cInfo.id)
val chat: Chat
val res: Boolean
if (i >= 0) {
chat = chats[i]
val pItem = chat.chatItems.lastOrNull()
if (pItem?.id == cItem.id) {
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
if (pItem.isRcvNew && !cItem.isRcvNew) {
// status changed from New to Read, update counter
decreaseCounterInChatNoContentTag(rhId, cInfo.id)
private fun chatItemBelongsToScope(cInfo: ChatInfo, cItem: ChatItem): Boolean =
when (secondaryContextFilter) {
null ->
chatId.value == cInfo.id && cInfo.groupChatScope() == null
is SecondaryContextFilter.GroupChatScopeContext -> {
val cInfoScope = cInfo.groupChatScope()
if (cInfoScope != null) {
chatId.value == cInfo.id && sameChatScope(cInfoScope, secondaryContextFilter.groupScopeInfo.toChatScope())
} else {
false
}
}
res = false
} else {
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
res = true
is SecondaryContextFilter.MsgContentTagContext ->
chatId.value == cInfo.id && cItem.isReport
}
return withContext(Dispatchers.Main) {
// update current chat
if (chatId.value == cInfo.id) {
suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean {
var itemAdded = false
// update chat list
if (cInfo.groupChatScope() == null) {
val i = getChatIndex(rhId, cInfo.id)
val chat: Chat
if (i >= 0) {
chat = chats[i]
val pItem = chat.chatItems.lastOrNull()
if (pItem?.id == cItem.id) {
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
if (pItem.isRcvNew && !cItem.isRcvNew) {
// status changed from New to Read, update counter
decreaseCounterInPrimaryContext(rhId, cInfo.id)
}
}
} else {
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
itemAdded = true
}
}
// update current scope
withContext(Dispatchers.Main) {
if (chatItemBelongsToScope(cInfo, cItem)) {
if (cItem.isDeletedContent || cItem.meta.itemDeleted != null) {
AudioPlayer.stop(cItem)
}
@@ -506,7 +540,6 @@ object ChatModel {
val itemIndex = items.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
items[itemIndex] = cItem
false
} else {
val status = chatItemStatuses.remove(cItem.id)
val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) {
@@ -515,44 +548,52 @@ object ChatModel {
cItem
}
addToChatItems(ci)
true
itemAdded = true
}
} else {
res
}
}
return itemAdded
}
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null, atIndex: Int? = null) {
// TODO [knocking] see diff:
// why chatItemStatuses gets updated even if chatId doesn't match?
// should chatItemBelongsToScope replace chatId check?
// should chatItemStatuses be updated only if chatItemBelongsToScope (as in changed code)? this seems to make more sense
withContext(Dispatchers.Main) {
if (chatId.value == cInfo.id) {
if (chatItemBelongsToScope(cInfo, cItem)) {
val items = chatItems.value
val itemIndex = atIndex ?: items.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
items[itemIndex] = cItem
}
} else if (status != null) {
chatItemStatuses[cItem.id] = status
if (status != null) {
chatItemStatuses[cItem.id] = status
}
}
}
}
// TODO [knocking] why does this function not use `withContext(Dispatchers.Main) { ... }` ?
fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
if (cItem.isRcvNew) {
decreaseCounterInChatNoContentTag(rhId, cInfo.id)
}
// update previews
val i = getChatIndex(rhId, cInfo.id)
val chat: Chat
if (i >= 0) {
chat = chats[i]
val pItem = chat.chatItems.lastOrNull()
if (pItem?.id == cItem.id) {
chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy))
// update chat list
if (cInfo.groupChatScope() == null) {
if (cItem.isRcvNew) {
decreaseCounterInPrimaryContext(rhId, cInfo.id)
}
// update preview
val i = getChatIndex(rhId, cInfo.id)
val chat: Chat
if (i >= 0) {
chat = chats[i]
val pItem = chat.chatItems.lastOrNull()
if (pItem?.id == cItem.id) {
chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy))
}
}
}
// remove from current chat
if (chatId.value == cInfo.id) {
// remove from current scope
if (chatItemBelongsToScope(cInfo, cItem)) {
chatItems.removeAllAndNotify {
// We delete taking into account meta.createdAt to make sure we will not be in situation when two items with the same id will be deleted
// (it can happen if already deleted chat item in backend still in the list and new one came with the same (re-used) chat item id)
@@ -585,7 +626,7 @@ object ChatModel {
Log.d(TAG, "exiting removeMemberItems")
return
}
val cInfo = ChatInfo.Group(groupInfo)
val cInfo = ChatInfo.Group(groupInfo, groupChatScope = null) // TODO [knocking] review
if (chatId.value == groupInfo.id) {
for (i in 0 until chatItems.value.size) {
val updatedItem = removedUpdatedItem(chatItems.value[i])
@@ -687,7 +728,7 @@ object ChatModel {
chats[chatIdx] = chat.copy(
chatStats = chat.chatStats.copy(unreadCount = unreadCount, unreadMentions = unreadMentions)
)
updateChatTagReadNoContentTag(chats[chatIdx], wasUnread)
updateChatTagReadInPrimaryContext(chats[chatIdx], wasUnread)
}
}
}
@@ -728,9 +769,9 @@ object ChatModel {
return markedRead to mentionsMarkedRead
}
private fun decreaseCounterInChatNoContentTag(rhId: Long?, chatId: ChatId) {
private fun decreaseCounterInPrimaryContext(rhId: Long?, chatId: ChatId) {
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
if (contentTag != null) return
if (secondaryContextFilter != null) return
val chatIndex = getChatIndex(rhId, chatId)
if (chatIndex == -1) return
@@ -744,7 +785,7 @@ object ChatModel {
unreadCount = unreadCount,
)
)
updateChatTagReadNoContentTag(chats[chatIndex], wasUnread)
updateChatTagReadInPrimaryContext(chats[chatIndex], wasUnread)
}
fun removeChat(rhId: Long?, id: String) {
@@ -813,16 +854,16 @@ object ChatModel {
}
fun increaseUnreadCounter(rhId: Long?, user: UserLike) {
changeUnreadCounterNoContentTag(rhId, user, 1)
changeUnreadCounterInPrimaryContext(rhId, user, 1)
}
fun decreaseUnreadCounter(rhId: Long?, user: UserLike, by: Int = 1) {
changeUnreadCounterNoContentTag(rhId, user, -by)
changeUnreadCounterInPrimaryContext(rhId, user, -by)
}
private fun changeUnreadCounterNoContentTag(rhId: Long?, user: UserLike, by: Int) {
private fun changeUnreadCounterInPrimaryContext(rhId: Long?, user: UserLike, by: Int) {
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
if (contentTag != null) return
if (secondaryContextFilter != null) return
val i = users.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == rhId }
if (i != -1) {
@@ -830,9 +871,9 @@ object ChatModel {
}
}
fun updateChatTagReadNoContentTag(chat: Chat, wasUnread: Boolean) {
fun updateChatTagReadInPrimaryContext(chat: Chat, wasUnread: Boolean) {
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
if (contentTag != null) return
if (secondaryContextFilter != null) return
val tags = chat.chatInfo.chatTags ?: return
val nowUnread = chat.unreadTag
@@ -842,21 +883,21 @@ object ChatModel {
unreadTags[tag] = (unreadTags[tag] ?: 0) + 1
}
} else if (!nowUnread && wasUnread) {
markChatTagReadNoContentTag_(chat, tags)
markChatTagReadInPrimaryContext_(chat, tags)
}
}
fun markChatTagRead(chat: Chat) {
if (chat.unreadTag) {
chat.chatInfo.chatTags?.let { tags ->
markChatTagReadNoContentTag_(chat, tags)
markChatTagReadInPrimaryContext_(chat, tags)
}
}
}
private fun markChatTagReadNoContentTag_(chat: Chat, tags: List<Long>) {
private fun markChatTagReadInPrimaryContext_(chat: Chat, tags: List<Long>) {
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
if (contentTag != null) return
if (secondaryContextFilter != null) return
for (tag in tags) {
val count = unreadTags[tag]
@@ -888,12 +929,12 @@ object ChatModel {
val wasReportsCount = chat.chatStats.reportsCount
val nowReportsCount = chats[i].chatStats.reportsCount
val by = if (wasReportsCount == 0 && nowReportsCount > 0) 1 else if (wasReportsCount > 0 && nowReportsCount == 0) -1 else 0
changeGroupReportsTagNoContentTag(by)
changeGroupReportsTagInPrimaryContext(by)
}
}
private fun changeGroupReportsTagNoContentTag(by: Int = 0) {
if (by == 0 || contentTag != null) return
private fun changeGroupReportsTagInPrimaryContext(by: Int = 0) {
if (by == 0 || secondaryContextFilter != null) return
presetTags[PresetTagKind.GROUP_REPORTS] = kotlin.math.max(0, (presetTags[PresetTagKind.GROUP_REPORTS] ?: 0) + by)
clearActiveChatFilterIfNeeded()
}
@@ -1098,6 +1139,28 @@ enum class ChatType(val type: String) {
ContactConnection(":");
}
sealed class GroupChatScope {
class MemberSupport(val groupMemberId_: Long?): GroupChatScope()
}
fun sameChatScope(scope1: GroupChatScope, scope2: GroupChatScope) =
scope1 is GroupChatScope.MemberSupport
&& scope2 is GroupChatScope.MemberSupport
&& scope1.groupMemberId_ == scope2.groupMemberId_
@Serializable
sealed class GroupChatScopeInfo {
@Serializable @SerialName("memberSupport") data class MemberSupport(val groupMember_: GroupMember?) : GroupChatScopeInfo()
fun toChatScope(): GroupChatScope =
when (this) {
is MemberSupport -> when (groupMember_) {
null -> GroupChatScope.MemberSupport(groupMemberId_ = null)
else -> GroupChatScope.MemberSupport(groupMemberId_ = groupMember_.groupMemberId)
}
}
}
@Serializable
data class User(
val remoteHostId: Long?,
@@ -1235,6 +1298,11 @@ data class Chat(
else -> false
}
val userIsPending: Boolean get() = when(chatInfo) {
is ChatInfo.Group -> chatInfo.groupInfo.membership.memberPending
else -> false
}
val unreadTag: Boolean get() = when (chatInfo.chatSettings?.enableNtfs) {
All -> chatStats.unreadChat || chatStats.unreadCount > 0
Mentions -> chatStats.unreadChat || chatStats.unreadMentions > 0
@@ -1299,7 +1367,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
}
@Serializable @SerialName("group")
data class Group(val groupInfo: GroupInfo): ChatInfo() {
data class Group(val groupInfo: GroupInfo, val groupChatScope: GroupChatScopeInfo?): ChatInfo() {
override val chatType get() = ChatType.Group
override val localDisplayName get() = groupInfo.localDisplayName
override val id get() = groupInfo.id
@@ -1318,7 +1386,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val localAlias get() = groupInfo.localAlias
companion object {
val sampleData = Group(GroupInfo.sampleData)
val sampleData = Group(GroupInfo.sampleData, groupChatScope = null)
}
}
@@ -1419,6 +1487,11 @@ sealed class ChatInfo: SomeChat, NamedChat {
}
}
fun groupChatScope(): GroupChatScope? = when (this) {
is Group -> groupChatScope?.toChatScope()
else -> null
}
fun ntfsEnabled(ci: ChatItem): Boolean =
ntfsEnabled(ci.meta.userMention)
@@ -1449,7 +1522,9 @@ sealed class ChatInfo: SomeChat, NamedChat {
val userCanSend: Boolean
get() = when (this) {
is ChatInfo.Direct -> true
is ChatInfo.Group -> groupInfo.membership.memberRole >= GroupMemberRole.Member
is ChatInfo.Group ->
(groupInfo.membership.memberRole >= GroupMemberRole.Member && !groupInfo.membership.memberPending)
|| groupChatScope != null
is ChatInfo.Local -> true
else -> false
}
@@ -1834,7 +1909,8 @@ data class GroupProfile (
val description: String? = null,
override val image: String? = null,
override val localAlias: String = "",
val groupPreferences: GroupPreferences? = null
val groupPreferences: GroupPreferences? = null,
val memberAdmission: GroupMemberAdmission? = null
): NamedChat {
companion object {
val sampleData = GroupProfile(
@@ -1844,6 +1920,27 @@ data class GroupProfile (
}
}
@Serializable
data class GroupMemberAdmission(
val review: MemberCriteria? = null,
) {
companion object {
val sampleData = GroupMemberAdmission(
review = null,
)
}
}
@Serializable
enum class MemberCriteria {
@SerialName("all") All;
val text: String
get() = when(this) {
MemberCriteria.All -> generalGetString(MR.strings.member_criteria_all)
}
}
@Serializable
data class BusinessChatInfo (
val chatType: BusinessChatType,
@@ -1872,7 +1969,8 @@ data class GroupMember (
val memberProfile: LocalProfile,
val memberContactId: Long? = null,
val memberContactProfileId: Long,
var activeConn: Connection? = null
var activeConn: Connection? = null,
val supportChat: GroupSupportChat? = null
): NamedChat {
val id: String get() = "#$groupId @$groupMemberId"
val ready get() = activeConn?.connStatus == ConnStatus.Ready
@@ -1931,6 +2029,7 @@ data class GroupMember (
GroupMemberStatus.MemUnknown -> false
GroupMemberStatus.MemInvited -> false
GroupMemberStatus.MemPendingApproval -> true
GroupMemberStatus.MemPendingReview -> true
GroupMemberStatus.MemIntroduced -> false
GroupMemberStatus.MemIntroInvited -> false
GroupMemberStatus.MemAccepted -> false
@@ -1948,6 +2047,7 @@ data class GroupMember (
GroupMemberStatus.MemUnknown -> false
GroupMemberStatus.MemInvited -> false
GroupMemberStatus.MemPendingApproval -> false
GroupMemberStatus.MemPendingReview -> false
GroupMemberStatus.MemIntroduced -> true
GroupMemberStatus.MemIntroInvited -> true
GroupMemberStatus.MemAccepted -> true
@@ -1957,6 +2057,12 @@ data class GroupMember (
GroupMemberStatus.MemCreator -> true
}
val memberPending: Boolean get() = when (this.memberStatus) {
GroupMemberStatus.MemPendingApproval -> true
GroupMemberStatus.MemPendingReview -> true
else -> false
}
fun canBeRemoved(groupInfo: GroupInfo): Boolean {
val userRole = groupInfo.membership.memberRole
return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft
@@ -1997,6 +2103,14 @@ data class GroupMember (
}
}
@Serializable
class GroupSupportChat (
val chatTs: Instant,
val unread: Int,
val memberAttention: Int,
val mentions: Int
)
@Serializable
data class GroupMemberSettings(val showMessages: Boolean) {}
@@ -2053,6 +2167,7 @@ enum class GroupMemberStatus {
@SerialName("unknown") MemUnknown,
@SerialName("invited") MemInvited,
@SerialName("pending_approval") MemPendingApproval,
@SerialName("pending_review") MemPendingReview,
@SerialName("introduced") MemIntroduced,
@SerialName("intro-inv") MemIntroInvited,
@SerialName("accepted") MemAccepted,
@@ -2069,6 +2184,7 @@ enum class GroupMemberStatus {
MemUnknown -> generalGetString(MR.strings.group_member_status_unknown)
MemInvited -> generalGetString(MR.strings.group_member_status_invited)
MemPendingApproval -> generalGetString(MR.strings.group_member_status_pending_approval)
MemPendingReview -> generalGetString(MR.strings.group_member_status_pending_review)
MemIntroduced -> generalGetString(MR.strings.group_member_status_introduced)
MemIntroInvited -> generalGetString(MR.strings.group_member_status_intro_invitation)
MemAccepted -> generalGetString(MR.strings.group_member_status_accepted)
@@ -2086,6 +2202,7 @@ enum class GroupMemberStatus {
MemUnknown -> generalGetString(MR.strings.group_member_status_unknown_short)
MemInvited -> generalGetString(MR.strings.group_member_status_invited)
MemPendingApproval -> generalGetString(MR.strings.group_member_status_pending_approval_short)
MemPendingReview -> generalGetString(MR.strings.group_member_status_pending_review_short)
MemIntroduced -> generalGetString(MR.strings.group_member_status_connecting)
MemIntroInvited -> generalGetString(MR.strings.group_member_status_connecting)
MemAccepted -> generalGetString(MR.strings.group_member_status_connecting)
@@ -2508,6 +2625,8 @@ data class ChatItem (
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
is RcvGroupEvent.MemberAdded -> false
is RcvGroupEvent.MemberConnected -> false
is RcvGroupEvent.MemberAccepted -> false
is RcvGroupEvent.UserAccepted -> false
is RcvGroupEvent.MemberLeft -> false
is RcvGroupEvent.MemberRole -> false
is RcvGroupEvent.MemberBlocked -> false
@@ -2700,6 +2819,11 @@ data class ChatItem (
}
}
sealed class SecondaryContextFilter {
class GroupChatScopeContext(val groupScopeInfo: GroupChatScopeInfo): SecondaryContextFilter()
class MsgContentTagContext(val contentTag: MsgContentTag): SecondaryContextFilter()
}
fun MutableState<SnapshotStateList<Chat>>.add(index: Int, elem: Chat) {
value = SnapshotStateList<Chat>().apply { addAll(value); add(index, elem) }
}
@@ -4133,6 +4257,8 @@ sealed class RcvDirectEvent() {
sealed class RcvGroupEvent() {
@Serializable @SerialName("memberAdded") class MemberAdded(val groupMemberId: Long, val profile: Profile): RcvGroupEvent()
@Serializable @SerialName("memberConnected") class MemberConnected(): RcvGroupEvent()
@Serializable @SerialName("memberAccepted") class MemberAccepted(val groupMemberId: Long, val profile: Profile): RcvGroupEvent()
@Serializable @SerialName("userAccepted") class UserAccepted(): RcvGroupEvent()
@Serializable @SerialName("memberLeft") class MemberLeft(): RcvGroupEvent()
@Serializable @SerialName("memberRole") class MemberRole(val groupMemberId: Long, val profile: Profile, val role: GroupMemberRole): RcvGroupEvent()
@Serializable @SerialName("memberBlocked") class MemberBlocked(val groupMemberId: Long, val profile: Profile, val blocked: Boolean): RcvGroupEvent()
@@ -4148,6 +4274,8 @@ sealed class RcvGroupEvent() {
val text: String get() = when (this) {
is MemberAdded -> String.format(generalGetString(MR.strings.rcv_group_event_member_added), profile.profileViewName)
is MemberConnected -> generalGetString(MR.strings.rcv_group_event_member_connected)
is MemberAccepted -> String.format(generalGetString(MR.strings.rcv_group_event_member_accepted), profile.profileViewName)
is UserAccepted -> generalGetString(MR.strings.rcv_group_event_user_accepted)
is MemberLeft -> generalGetString(MR.strings.rcv_group_event_member_left)
is MemberRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_member_role), profile.profileViewName, role.text)
is MemberBlocked -> if (blocked) {
@@ -903,8 +903,8 @@ object ChatController {
return null
}
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, contentTag: MsgContentTag? = null, pagination: ChatPagination, search: String = ""): Pair<Chat, NavigationInfo>? {
val r = sendCmd(rh, CC.ApiGetChat(type, id, contentTag, pagination, search))
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, contentTag: MsgContentTag? = null, pagination: ChatPagination, search: String = ""): Pair<Chat, NavigationInfo>? {
val r = sendCmd(rh, CC.ApiGetChat(type, id, scope, contentTag, pagination, search))
if (r is CR.ApiChat) return if (rh == null) r.chat to r.navInfo else r.chat.copy(remoteHostId = rh) to r.navInfo
Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}")
if (pagination is ChatPagination.Around && r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.ChatItemNotFound) {
@@ -937,8 +937,8 @@ object ChatController {
suspend fun apiReorderChatTags(rh: Long?, tagIds: List<Long>) = sendCommandOkResp(rh, CC.ApiReorderChatTags(tagIds))
suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, live: Boolean = false, ttl: Int? = null, composedMessages: List<ComposedMessage>): List<AChatItem>? {
val cmd = CC.ApiSendMessages(type, id, live, ttl, composedMessages)
suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, live: Boolean = false, ttl: Int? = null, composedMessages: List<ComposedMessage>): List<AChatItem>? {
val cmd = CC.ApiSendMessages(type, id, scope, live, ttl, composedMessages)
return processSendMessageCmd(rh, cmd)
}
@@ -996,8 +996,8 @@ object ChatController {
}
}
suspend fun apiGetChatItemInfo(rh: Long?, type: ChatType, id: Long, itemId: Long): ChatItemInfo? {
return when (val r = sendCmd(rh, CC.ApiGetChatItemInfo(type, id, itemId))) {
suspend fun apiGetChatItemInfo(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemId: Long): ChatItemInfo? {
return when (val r = sendCmd(rh, CC.ApiGetChatItemInfo(type, id, scope, itemId))) {
is CR.ApiChatItemInfo -> r.chatItemInfo
else -> {
apiErrorAlert("apiGetChatItemInfo", generalGetString(MR.strings.error_loading_details), r)
@@ -1006,13 +1006,13 @@ object ChatController {
}
}
suspend fun apiForwardChatItems(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemIds: List<Long>, ttl: Int?): List<ChatItem>? {
val cmd = CC.ApiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl)
suspend fun apiForwardChatItems(rh: Long?, toChatType: ChatType, toChatId: Long, toScope: GroupChatScope?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, itemIds: List<Long>, ttl: Int?): List<ChatItem>? {
val cmd = CC.ApiForwardChatItems(toChatType, toChatId, toScope, fromChatType, fromChatId, fromScope, itemIds, ttl)
return processSendMessageCmd(rh, cmd)?.map { it.chatItem }
}
suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, chatItemIds: List<Long>): CR.ForwardPlan? {
return when (val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, chatItemIds))) {
suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, chatItemIds: List<Long>): CR.ForwardPlan? {
return when (val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, fromScope, chatItemIds))) {
is CR.ForwardPlan -> r
else -> {
apiErrorAlert("apiPlanForwardChatItems", generalGetString(MR.strings.error_forwarding_messages), r)
@@ -1021,8 +1021,8 @@ object ChatController {
}
}
suspend fun apiUpdateChatItem(rh: Long?, type: ChatType, id: Long, itemId: Long, updatedMessage: UpdatedMessage, live: Boolean = false): AChatItem? {
val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, itemId, updatedMessage, live))
suspend fun apiUpdateChatItem(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemId: Long, updatedMessage: UpdatedMessage, live: Boolean = false): AChatItem? {
val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, scope, itemId, updatedMessage, live))
when {
r is CR.ChatItemUpdated -> return r.chatItem
r is CR.ChatItemNotChanged -> return r.chatItem
@@ -1044,8 +1044,8 @@ object ChatController {
return null
}
suspend fun apiChatItemReaction(rh: Long?, type: ChatType, id: Long, itemId: Long, add: Boolean, reaction: MsgReaction): ChatItem? {
val r = sendCmd(rh, CC.ApiChatItemReaction(type, id, itemId, add, reaction))
suspend fun apiChatItemReaction(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemId: Long, add: Boolean, reaction: MsgReaction): ChatItem? {
val r = sendCmd(rh, CC.ApiChatItemReaction(type, id, scope, itemId, add, reaction))
if (r is CR.ChatItemReaction) return r.reaction.chatReaction.chatItem
Log.e(TAG, "apiUpdateChatItem bad response: ${r.responseType} ${r.details}")
return null
@@ -1059,8 +1059,8 @@ object ChatController {
return null
}
suspend fun apiDeleteChatItems(rh: Long?, type: ChatType, id: Long, itemIds: List<Long>, mode: CIDeleteMode): List<ChatItemDeletion>? {
val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, itemIds, mode))
suspend fun apiDeleteChatItems(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemIds: List<Long>, mode: CIDeleteMode): List<ChatItemDeletion>? {
val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, scope, itemIds, mode))
if (r is CR.ChatItemsDeleted) return r.chatItemDeletions
Log.e(TAG, "apiDeleteChatItem bad response: ${r.responseType} ${r.details}")
return null
@@ -1761,15 +1761,15 @@ object ChatController {
return null
}
suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long): Boolean {
val r = sendCmd(rh, CC.ApiChatRead(type, id))
suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?): Boolean {
val r = sendCmd(rh, CC.ApiChatRead(type, id, scope))
if (r is CR.CmdOk) return true
Log.e(TAG, "apiChatRead bad response: ${r.responseType} ${r.details}")
return false
}
suspend fun apiChatItemsRead(rh: Long?, type: ChatType, id: Long, itemIds: List<Long>): Boolean {
val r = sendCmd(rh, CC.ApiChatItemsRead(type, id, itemIds))
suspend fun apiChatItemsRead(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemIds: List<Long>): Boolean {
val r = sendCmd(rh, CC.ApiChatItemsRead(type, id, scope, itemIds))
if (r is CR.CmdOk) return true
Log.e(TAG, "apiChatItemsRead bad response: ${r.responseType} ${r.details}")
return false
@@ -2007,6 +2007,19 @@ object ChatController {
}
}
suspend fun apiAcceptMember(rh: Long?, groupId: Long, groupMemberId: Long, memberRole: GroupMemberRole): GroupMember? {
val r = sendCmd(rh, CC.ApiAcceptMember(groupId, groupMemberId, memberRole))
return when (r) {
is CR.MemberAccepted -> r.member
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiAcceptMember", generalGetString(MR.strings.error_accepting_member), r)
}
null
}
}
}
suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List<Long>, withMessages: Boolean = false): List<GroupMember>? =
when (val r = sendCmd(rh, CC.ApiRemoveMembers(groupId, memberIds, withMessages))) {
is CR.UserDeletedMembers -> r.members
@@ -2543,14 +2556,13 @@ object ChatController {
if (active(r.user)) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.addChatItem(rhId, cInfo, cItem)
// TODO [knocking] increase support chats unread count; move `isActiveReport` checks inside model functions?
if (cItem.isActiveReport) {
chatModel.chatsContext.increaseGroupReportsCounter(rhId, cInfo.id)
}
}
withContext(Dispatchers.Main) {
if (cItem.isReport) {
chatModel.secondaryChatsContext.value?.addChatItem(rhId, cInfo, cItem)
}
chatModel.secondaryChatsContext.value?.addChatItem(rhId, cInfo, cItem)
}
} else if (cItem.isRcvNew && cInfo.ntfsEnabled(cItem)) {
withContext(Dispatchers.Main) {
@@ -2579,9 +2591,7 @@ object ChatController {
chatModel.chatsContext.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
}
withContext(Dispatchers.Main) {
if (cItem.isReport) {
chatModel.secondaryChatsContext.value?.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
}
chatModel.secondaryChatsContext.value?.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
}
}
}
@@ -2593,9 +2603,7 @@ object ChatController {
chatModel.chatsContext.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
}
withContext(Dispatchers.Main) {
if (r.reaction.chatReaction.chatItem.isReport) {
chatModel.secondaryChatsContext.value?.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
}
chatModel.secondaryChatsContext.value?.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
}
}
}
@@ -2638,12 +2646,10 @@ object ChatController {
}
}
withContext(Dispatchers.Main) {
if (cItem.isReport) {
if (toChatItem == null) {
chatModel.secondaryChatsContext.value?.removeChatItem(rhId, cInfo, cItem)
} else {
chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, toChatItem.chatItem)
}
if (toChatItem == null) {
chatModel.secondaryChatsContext.value?.removeChatItem(rhId, cInfo, cItem)
} else {
chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, toChatItem.chatItem)
}
}
}
@@ -2703,6 +2709,13 @@ object ChatController {
chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
}
}
is CR.MemberAcceptedByOther ->
if (active(r.user)) {
// TODO [knocking] update secondary context?
withContext(Dispatchers.Main) {
chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
}
}
is CR.DeletedMemberUser -> // TODO update user member
if (active(r.user)) {
withContext(Dispatchers.Main) {
@@ -2783,6 +2796,16 @@ object ChatController {
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
}
if (
chatModel.chatId.value == r.groupInfo.id
&& ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)
&& chatModel.secondaryChatsContext.value?.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext
) {
withContext(Dispatchers.Main) {
chatModel.secondaryChatsContext.value = null
}
ModalManager.end.closeModals()
}
}
is CR.JoinedGroupMember ->
if (active(r.user)) {
@@ -3143,11 +3166,11 @@ object ChatController {
if (activeUser(rh, user)) {
val cInfo = aChatItem.chatInfo
val cItem = aChatItem.chatItem
withContext(Dispatchers.Main) { chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) }
withContext(Dispatchers.Main) {
if (cItem.isReport) {
chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem)
}
chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem)
}
withContext(Dispatchers.Main) {
chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem)
}
}
}
@@ -3159,7 +3182,7 @@ object ChatController {
chatModel.users.addAll(users)
return
}
val cInfo = ChatInfo.Group(r.groupInfo)
val cInfo = ChatInfo.Group(r.groupInfo, groupChatScope = null) // TODO [knocking] get scope from items?
withContext(Dispatchers.Main) {
val chatsCtx = chatModel.chatsContext
r.chatItemIDs.forEach { itemId ->
@@ -3214,11 +3237,11 @@ object ChatController {
if (!activeUser(rh, user)) {
notify()
} else {
val createdChat = withContext(Dispatchers.Main) { chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) }
val createdChat = withContext(Dispatchers.Main) {
chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem)
}
withContext(Dispatchers.Main) {
if (cItem.content.msgContent is MsgContent.MCReport) {
chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem)
}
chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem)
}
if (createdChat) {
notify()
@@ -3423,9 +3446,9 @@ sealed class CC {
class ApiGetSettings(val settings: AppSettings): CC()
class ApiGetChatTags(val userId: Long): CC()
class ApiGetChats(val userId: Long): CC()
class ApiGetChat(val type: ChatType, val id: Long, val contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC()
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC()
class ApiSendMessages(val type: ChatType, val id: Long, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC()
class ApiGetChat(val type: ChatType, val id: Long, val scope: GroupChatScope?, val contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC()
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long): CC()
class ApiSendMessages(val type: ChatType, val id: Long, val scope: GroupChatScope?, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC()
class ApiCreateChatTag(val tag: ChatTagData): CC()
class ApiSetChatTags(val type: ChatType, val id: Long, val tagIds: List<Long>): CC()
class ApiDeleteChatTag(val tagId: Long): CC()
@@ -3433,18 +3456,19 @@ sealed class CC {
class ApiReorderChatTags(val tagIds: List<Long>): CC()
class ApiCreateChatItems(val noteFolderId: Long, val composedMessages: List<ComposedMessage>): CC()
class ApiReportMessage(val groupId: Long, val chatItemId: Long, val reportReason: ReportReason, val reportText: String): CC()
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val updatedMessage: UpdatedMessage, val live: Boolean): CC()
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List<Long>, val mode: CIDeleteMode): CC()
class ApiUpdateChatItem(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long, val updatedMessage: UpdatedMessage, val live: Boolean): CC()
class ApiDeleteChatItem(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemIds: List<Long>, val mode: CIDeleteMode): CC()
class ApiDeleteMemberChatItem(val groupId: Long, val itemIds: List<Long>): CC()
class ApiArchiveReceivedReports(val groupId: Long): CC()
class ApiDeleteReceivedReports(val groupId: Long, val itemIds: List<Long>, val mode: CIDeleteMode): CC()
class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
class ApiChatItemReaction(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
class ApiGetReactionMembers(val userId: Long, val groupId: Long, val itemId: Long, val reaction: MsgReaction): CC()
class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val chatItemIds: List<Long>): CC()
class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemIds: List<Long>, val ttl: Int?): CC()
class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val chatItemIds: List<Long>): CC()
class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val itemIds: List<Long>, val ttl: Int?): CC()
class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC()
class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC()
class ApiJoinGroup(val groupId: Long): CC()
class ApiAcceptMember(val groupId: Long, val groupMemberId: Long, val memberRole: GroupMemberRole): CC()
class ApiMembersRole(val groupId: Long, val memberIds: List<Long>, val memberRole: GroupMemberRole): CC()
class ApiBlockMembersForAll(val groupId: Long, val memberIds: List<Long>, val blocked: Boolean): CC()
class ApiRemoveMembers(val groupId: Long, val memberIds: List<Long>, val withMessages: Boolean): CC()
@@ -3522,8 +3546,8 @@ sealed class CC {
class ApiGetNetworkStatuses(): CC()
class ApiAcceptContact(val incognito: Boolean, val contactReqId: Long): CC()
class ApiRejectContact(val contactReqId: Long): CC()
class ApiChatRead(val type: ChatType, val id: Long): CC()
class ApiChatItemsRead(val type: ChatType, val id: Long, val itemIds: List<Long>): CC()
class ApiChatRead(val type: ChatType, val id: Long, val scope: GroupChatScope?): CC()
class ApiChatItemsRead(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemIds: List<Long>): CC()
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
class ReceiveFile(val fileId: Long, val userApprovedRelays: Boolean, val encrypt: Boolean, val inline: Boolean?): CC()
class CancelFile(val fileId: Long): CC()
@@ -3595,16 +3619,16 @@ sealed class CC {
} else {
" content=${contentTag.name.lowercase()}"
}
"/_get chat ${chatRef(type, id)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
"/_get chat ${chatRef(type, id, scope)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
}
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id, scope)} $itemId"
is ApiSendMessages -> {
val msgs = json.encodeToString(composedMessages)
val ttlStr = if (ttl != null) "$ttl" else "default"
"/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json $msgs"
"/_send ${chatRef(type, id, scope)} live=${onOff(live)} ttl=${ttlStr} json $msgs"
}
is ApiCreateChatTag -> "/_create tag ${json.encodeToString(tag)}"
is ApiSetChatTags -> "/_tags ${chatRef(type, id)} ${tagIds.joinToString(",")}"
is ApiSetChatTags -> "/_tags ${chatRef(type, id, scope = null)} ${tagIds.joinToString(",")}"
is ApiDeleteChatTag -> "/_delete tag $tagId"
is ApiUpdateChatTag -> "/_update tag $tagId ${json.encodeToString(tagData)}"
is ApiReorderChatTags -> "/_reorder tags ${tagIds.joinToString(",")}"
@@ -3613,23 +3637,24 @@ sealed class CC {
"/_create *$noteFolderId json $msgs"
}
is ApiReportMessage -> "/_report #$groupId $chatItemId reason=${json.encodeToString(reportReason).trim('"')} $reportText"
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${updatedMessage.cmdString}"
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} ${itemIds.joinToString(",")} ${mode.deleteMode}"
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id, scope)} $itemId live=${onOff(live)} ${updatedMessage.cmdString}"
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id, scope)} ${itemIds.joinToString(",")} ${mode.deleteMode}"
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId ${itemIds.joinToString(",")}"
is ApiArchiveReceivedReports -> "/_archive reports #$groupId"
is ApiDeleteReceivedReports -> "/_delete reports #$groupId ${itemIds.joinToString(",")} ${mode.deleteMode}"
is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}"
is ApiChatItemReaction -> "/_reaction ${chatRef(type, id, scope)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}"
is ApiGetReactionMembers -> "/_reaction members $userId #$groupId $itemId ${json.encodeToString(reaction)}"
is ApiForwardChatItems -> {
val ttlStr = if (ttl != null) "$ttl" else "default"
"/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} ${itemIds.joinToString(",")} ttl=${ttlStr}"
"/_forward ${chatRef(toChatType, toChatId, toScope)} ${chatRef(fromChatType, fromChatId, fromScope)} ${itemIds.joinToString(",")} ttl=${ttlStr}"
}
is ApiPlanForwardChatItems -> {
"/_forward plan ${chatRef(fromChatType, fromChatId)} ${chatItemIds.joinToString(",")}"
"/_forward plan ${chatRef(fromChatType, fromChatId, fromScope)} ${chatItemIds.joinToString(",")}"
}
is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}"
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
is ApiJoinGroup -> "/_join #$groupId"
is ApiAcceptMember -> "/_accept member #$groupId $groupMemberId ${memberRole.memberRole}"
is ApiMembersRole -> "/_member role #$groupId ${memberIds.joinToString(",")} ${memberRole.memberRole}"
is ApiBlockMembersForAll -> "/_block #$groupId ${memberIds.joinToString(",")} blocked=${onOff(blocked)}"
is ApiRemoveMembers -> "/_remove #$groupId ${memberIds.joinToString(",")} messages=${onOff(withMessages)}"
@@ -3653,13 +3678,13 @@ sealed class CC {
is ApiAcceptConditions -> "/_accept_conditions ${conditionsId} ${operatorIds.joinToString(",")}"
is APISetChatItemTTL -> "/_ttl $userId ${chatItemTTLStr(seconds)}"
is APIGetChatItemTTL -> "/_ttl $userId"
is APISetChatTTL -> "/_ttl $userId ${chatRef(chatType, id)} ${chatItemTTLStr(seconds)}"
is APISetChatTTL -> "/_ttl $userId ${chatRef(chatType, id, scope = null)} ${chatItemTTLStr(seconds)}"
is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}"
is APIGetNetworkConfig -> "/network"
is APISetNetworkInfo -> "/_network info ${json.encodeToString(networkInfo)}"
is ReconnectServer -> "/reconnect $userId $server"
is ReconnectAllServers -> "/reconnect"
is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}"
is APISetChatSettings -> "/_settings ${chatRef(type, id, scope = null)} ${json.encodeToString(chatSettings)}"
is ApiSetMemberSettings -> "/_member settings #$groupId $groupMemberId ${json.encodeToString(memberSettings)}"
is APIContactInfo -> "/_info @$contactId"
is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId"
@@ -3681,8 +3706,8 @@ sealed class CC {
is APIConnectPlan -> "/_connect plan $userId $connLink"
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} ${connLink.connFullLink} ${connLink.connShortLink ?: ""}"
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}"
is ApiClearChat -> "/_clear chat ${chatRef(type, id)}"
is ApiDeleteChat -> "/_delete ${chatRef(type, id, scope = null)} ${chatDeleteMode.cmdString}"
is ApiClearChat -> "/_clear chat ${chatRef(type, id, scope = null)}"
is ApiListContacts -> "/_contacts $userId"
is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}"
is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}"
@@ -3707,9 +3732,9 @@ sealed class CC {
is ApiEndCall -> "/_call end @${contact.apiId}"
is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}"
is ApiGetNetworkStatuses -> "/_network_statuses"
is ApiChatRead -> "/_read chat ${chatRef(type, id)}"
is ApiChatItemsRead -> "/_read chat items ${chatRef(type, id)} ${itemIds.joinToString(",")}"
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
is ApiChatRead -> "/_read chat ${chatRef(type, id, scope)}"
is ApiChatItemsRead -> "/_read chat items ${chatRef(type, id, scope)} ${itemIds.joinToString(",")}"
is ApiChatUnread -> "/_unread chat ${chatRef(type, id, scope = null)} ${onOff(unreadChat)}"
is ReceiveFile ->
"/freceive $fileId" +
(" approved_relays=${onOff(userApprovedRelays)}") +
@@ -3793,6 +3818,7 @@ sealed class CC {
is ApiNewGroup -> "apiNewGroup"
is ApiAddMember -> "apiAddMember"
is ApiJoinGroup -> "apiJoinGroup"
is ApiAcceptMember -> "apiAcceptMember"
is ApiMembersRole -> "apiMembersRole"
is ApiBlockMembersForAll -> "apiBlockMembersForAll"
is ApiRemoveMembers -> "apiRemoveMembers"
@@ -3929,7 +3955,13 @@ sealed class CC {
private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd)
companion object {
fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}"
fun chatRef(chatType: ChatType, id: Long, scope: GroupChatScope?) = when (scope) {
null -> "${chatType.type}${id}"
is GroupChatScope.MemberSupport -> when (scope.groupMemberId_) {
null -> "${chatType.type}${id}(_support)"
else -> "${chatType.type}${id}(_support:${scope.groupMemberId_})"
}
}
}
}
@@ -5846,6 +5878,8 @@ sealed class CR {
@Serializable @SerialName("receivedGroupInvitation") class ReceivedGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val memberRole: GroupMemberRole): CR()
@Serializable @SerialName("groupDeletedUser") class GroupDeletedUser(val user: UserRef, val groupInfo: GroupInfo): CR()
@Serializable @SerialName("joinedGroupMemberConnecting") class JoinedGroupMemberConnecting(val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember, val member: GroupMember): CR()
@Serializable @SerialName("memberAccepted") class MemberAccepted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("memberAcceptedByOther") class MemberAcceptedByOther(val user: UserRef, val groupInfo: GroupInfo, val acceptingMember: GroupMember, val member: GroupMember): CR()
@Serializable @SerialName("memberRole") class MemberRole(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR()
@Serializable @SerialName("membersRoleUser") class MembersRoleUser(val user: UserRef, val groupInfo: GroupInfo, val members: List<GroupMember>, val toRole: GroupMemberRole): CR()
@Serializable @SerialName("memberBlockedForAll") class MemberBlockedForAll(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val blocked: Boolean): CR()
@@ -6031,6 +6065,8 @@ sealed class CR {
is ReceivedGroupInvitation -> "receivedGroupInvitation"
is GroupDeletedUser -> "groupDeletedUser"
is JoinedGroupMemberConnecting -> "joinedGroupMemberConnecting"
is MemberAccepted -> "memberAccepted"
is MemberAcceptedByOther -> "memberAcceptedByOther"
is MemberRole -> "memberRole"
is MembersRoleUser -> "membersRoleUser"
is MemberBlockedForAll -> "memberBlockedForAll"
@@ -6209,6 +6245,8 @@ sealed class CR {
is ReceivedGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmemberRole: $memberRole")
is GroupDeletedUser -> withUser(user, json.encodeToString(groupInfo))
is JoinedGroupMemberConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember\nmember: $member")
is MemberAccepted -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
is MemberAcceptedByOther -> withUser(user, "groupInfo: $groupInfo\nacceptingMember: $acceptingMember\nmember: $member")
is MemberRole -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nfromRole: $fromRole\ntoRole: $toRole")
is MembersRoleUser -> withUser(user, "groupInfo: $groupInfo\nmembers: $members\ntoRole: $toRole")
is MemberBlockedForAll -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nblocked: $blocked")
@@ -16,6 +16,7 @@ expect fun PlatformTextField(
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
userIsPending: Boolean,
placeholder: String,
showVoiceButton: Boolean,
onMessageChange: (ComposeMessage) -> Unit,
@@ -104,6 +104,7 @@ fun TerminalLayout(
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false,
userIsObserver = false,
userIsPending = false,
userCanSend = true,
allowVoiceToContact = {},
placeholder = "",
@@ -40,9 +40,8 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.group.ChatTTLSection
import chat.simplex.common.views.chat.group.ChatTTLOption
import chat.simplex.common.views.chatlist.updateChatSettings
import chat.simplex.common.views.database.*
import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
import kotlinx.coroutines.*
@@ -617,7 +616,10 @@ fun ChatInfoLayout(
}
SectionDividerSpaced(maxBottomPadding = false)
ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems)
SectionView {
ChatTTLOption(chatItemTTL, setChatItemTTL, deletingItems)
SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer))
}
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
val conn = contact.activeConn
@@ -1384,7 +1386,7 @@ private fun setChatTTL(
private suspend fun afterSetChatTTL(chatsCtx: ChatModel.ChatsContext, rhId: Long?, chatInfo: ChatInfo, progressIndicator: MutableState<Boolean>) {
try {
val pagination = ChatPagination.Initial(ChatPagination.INITIAL_COUNT)
val (chat, navInfo) = controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, null, pagination) ?: return
val (chat, navInfo) = controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, scope = null, contentTag = null, pagination) ?: return
if (chat.chatItems.isEmpty()) {
// replacing old chat with the same old chat but without items. Less intrusive way of clearing a preview
withContext(Dispatchers.Main) {
@@ -17,7 +17,7 @@ suspend fun apiLoadSingleMessage(
apiId: Long,
itemId: Long
): ChatItem? = coroutineScope {
val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.contentTag, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null
val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), chatsCtx.contentTag, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null
chat.chatItems.firstOrNull()
}
@@ -31,7 +31,7 @@ suspend fun apiLoadMessages(
openAroundItemId: Long? = null,
visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 }
) = coroutineScope {
val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.contentTag, pagination, search) ?: return@coroutineScope
val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), chatsCtx.contentTag, pagination, search) ?: return@coroutineScope
// For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes
/** When [openAroundItemId] is provided, chatId can be different too */
if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last && openAroundItemId == null)
@@ -54,7 +54,7 @@ suspend fun processLoadedChat(
when (pagination) {
is ChatPagination.Initial -> {
val newSplits = if (chat.chatItems.isNotEmpty() && navInfo.afterTotal > 0) listOf(chat.chatItems.last().id) else emptyList()
if (chatsCtx.contentTag == null) {
if (chatsCtx.secondaryContextFilter == null) {
// update main chats, not content tagged
withContext(Dispatchers.Main) {
val oldChat = chatModel.chatsContext.getChat(chat.id)
@@ -0,0 +1,116 @@
package chat.simplex.common.views.chat
import SectionItemView
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.chat.group.removeMemberDialog
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun ComposeContextPendingMemberActionsView(
rhId: Long?,
groupInfo: GroupInfo,
member: GroupMember
) {
Column(
Modifier
.height(60.dp)
.background(MaterialTheme.colors.surface)
) {
Divider()
Row(
Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Column(
Modifier
.fillMaxWidth()
.fillMaxHeight()
.weight(1F)
.clickable {
removeMemberDialog(rhId, groupInfo, member, chatModel, close = { ModalManager.end.closeModal() })
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(stringResource(MR.strings.remove_pending_member_button), color = Color.Red)
}
Column(
Modifier
.fillMaxWidth()
.fillMaxHeight()
.weight(1F)
.clickable {
acceptMemberDialog(rhId, groupInfo, member, close = { ModalManager.end.closeModal() })
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(stringResource(MR.strings.accept_pending_member_button), color = MaterialTheme.colors.primary)
}
}
}
}
fun acceptMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.accept_pending_member_alert_title),
text = generalGetString(MR.strings.accept_pending_member_alert_question),
buttons = {
Column {
// Accept as member
SectionItemView({
AlertManager.shared.hideAlert()
acceptMember(rhId, groupInfo, member, GroupMemberRole.Member, close)
}) {
Text(generalGetString(MR.strings.accept_pending_member_alert_confirmation_as_member), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
// Accept as observer
SectionItemView({
AlertManager.shared.hideAlert()
acceptMember(rhId, groupInfo, member, GroupMemberRole.Observer, close)
}) {
Text(generalGetString(MR.strings.accept_pending_member_alert_confirmation_as_observer), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
// Cancel
SectionItemView({
AlertManager.shared.hideAlert()
}) {
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
}
)
}
private fun acceptMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, role: GroupMemberRole, close: (() -> Unit)?) {
withBGApi {
val acceptedMember = chatModel.controller.apiAcceptMember(rhId, groupInfo.groupId, member.groupMemberId, role)
if (acceptedMember != null) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, acceptedMember)
}
withContext(Dispatchers.Main) {
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, acceptedMember)
}
}
close?.invoke()
}
}
@@ -333,7 +333,9 @@ suspend fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text:
@Composable
fun ComposeView(
rhId: Long?,
chatModel: ChatModel,
chatsCtx: ChatModel.ChatsContext,
chat: Chat,
composeState: MutableState<ComposeState>,
attachmentOption: MutableState<AttachmentOption?>,
@@ -466,6 +468,7 @@ fun ComposeView(
rh = chat.remoteHostId,
type = cInfo.chatType,
id = cInfo.apiId,
scope = cInfo.groupChatScope(),
live = live,
ttl = ttl,
composedMessages = listOf(ComposedMessage(file, quoted, mc, mentions))
@@ -473,7 +476,7 @@ fun ComposeView(
if (!chatItems.isNullOrEmpty()) {
chatItems.forEach { aChatItem ->
withContext(Dispatchers.Main) {
chatModel.chatsContext.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
chatsCtx.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
}
}
return chatItems.first().chatItem
@@ -498,15 +501,17 @@ fun ComposeView(
rh = rhId,
toChatType = chat.chatInfo.chatType,
toChatId = chat.chatInfo.apiId,
toScope = chat.chatInfo.groupChatScope(),
fromChatType = fromChatInfo.chatType,
fromChatId = fromChatInfo.apiId,
fromScope = fromChatInfo.groupChatScope(),
itemIds = forwardedItem.map { it.id },
ttl = ttl
)
withContext(Dispatchers.Main) {
chatItems?.forEach { chatItem ->
chatModel.chatsContext.addChatItem(rhId, chat.chatInfo, chatItem)
chatsCtx.addChatItem(rhId, chat.chatInfo, chatItem)
}
}
@@ -566,9 +571,10 @@ fun ComposeView(
suspend fun sendReport(reportReason: ReportReason, chatItemId: Long): List<ChatItem>? {
val cItems = chatModel.controller.apiReportMessage(chat.remoteHostId, chat.chatInfo.apiId, chatItemId, reportReason, msgText)
if (cItems != null) {
// TODO [knocking] create report chat items in support scope
withContext(Dispatchers.Main) {
cItems.forEach { chatItem ->
chatModel.chatsContext.addChatItem(chat.remoteHostId, chat.chatInfo, chatItem.chatItem)
chatsCtx.addChatItem(chat.remoteHostId, chat.chatInfo, chatItem.chatItem)
}
}
}
@@ -581,7 +587,7 @@ fun ComposeView(
val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc)
if (contact != null) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateContact(chat.remoteHostId, contact)
chatsCtx.updateContact(chat.remoteHostId, contact)
}
}
}
@@ -594,13 +600,14 @@ fun ComposeView(
rh = chat.remoteHostId,
type = cInfo.chatType,
id = cInfo.apiId,
scope = cInfo.groupChatScope(),
itemId = ei.meta.itemId,
updatedMessage = UpdatedMessage(updateMsgContent(oldMsgContent), cs.memberMentions),
live = live
)
if (updatedItem != null) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem)
chatsCtx.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem)
}
}
return updatedItem?.chatItem
@@ -891,7 +898,7 @@ fun ComposeView(
fun editPrevMessage() {
if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return
val lastEditable = chatModel.chatsContext.chatItems.value.findLast { it.meta.editable }
val lastEditable = chatsCtx.chatItems.value.findLast { it.meta.editable }
if (lastEditable != null) {
composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews)
}
@@ -1002,9 +1009,25 @@ fun ComposeView(
val userCanSend = rememberUpdatedState(chat.chatInfo.userCanSend)
val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled)
val userIsObserver = rememberUpdatedState(chat.userIsObserver)
val userIsPending = rememberUpdatedState(chat.userIsPending)
val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv)
Column {
if (
chat.chatInfo is ChatInfo.Group
&& chatsCtx.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext
&& chatsCtx.secondaryContextFilter.groupScopeInfo is GroupChatScopeInfo.MemberSupport
&& chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_ != null
&& chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_.memberPending
&& composeState.value.contextItem == ComposeContextItem.NoContextItem
&& composeState.value.preview == ComposePreview.NoPreview
) {
ComposeContextPendingMemberActionsView(
rhId = rhId,
groupInfo = chat.chatInfo.groupInfo,
member = chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_
)
}
if (nextSendGrpInv.value) {
ComposeContextInvitingContactMemberView()
}
@@ -1164,7 +1187,8 @@ fun ComposeView(
needToAllowVoiceToContact,
allowedVoiceByPrefs,
allowVoiceToContact = ::allowVoiceToContact,
userIsObserver = userIsObserver.value,
userIsObserver = if (chatsCtx.secondaryContextFilter == null) userIsObserver.value else false,
userIsPending = if (chatsCtx.secondaryContextFilter == null) userIsPending.value else false,
userCanSend = userCanSend.value,
sendButtonColor = sendButtonColor,
timedMessageAllowed = timedMessageAllowed,
@@ -77,7 +77,7 @@ fun SelectedItemsButtonsToolbar(
val forwardCountProhibited = remember { mutableStateOf(false) }
Box {
// It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty
ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() })
ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() })
Row(
Modifier
.matchParentSize()
@@ -15,9 +15,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
@@ -46,6 +44,7 @@ fun SendMsgView(
needToAllowVoiceToContact: Boolean,
allowedVoiceByPrefs: Boolean,
userIsObserver: Boolean,
userIsPending: Boolean,
userCanSend: Boolean,
sendButtonColor: Color = MaterialTheme.colors.primary,
allowVoiceToContact: () -> Unit,
@@ -90,6 +89,7 @@ fun SendMsgView(
textStyle,
showDeleteTextButton,
userIsObserver,
userIsPending,
if (clicksOnTextFieldDisabled) "" else placeholder,
showVoiceButton,
onMessageChange,
@@ -581,6 +581,7 @@ fun PreviewSendMsgView() {
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
userIsObserver = false,
userIsPending = false,
userCanSend = true,
allowVoiceToContact = {},
timedMessageAllowed = false,
@@ -617,6 +618,7 @@ fun PreviewSendMsgViewEditing() {
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
userIsObserver = false,
userIsPending = false,
userCanSend = true,
allowVoiceToContact = {},
timedMessageAllowed = false,
@@ -653,6 +655,7 @@ fun PreviewSendMsgViewInProgress() {
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
userIsObserver = false,
userIsPending = false,
userCanSend = true,
allowVoiceToContact = {},
timedMessageAllowed = false,
@@ -144,7 +144,7 @@ fun AddGroupMembersLayout(
horizontalArrangement = Arrangement.Center
) {
ChatInfoToolbarTitle(
ChatInfo.Group(groupInfo),
ChatInfo.Group(groupInfo, groupChatScope = null),
imageSize = 60.dp,
iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight
)
@@ -74,6 +74,11 @@ fun ModalData.GroupChatInfoView(
val chatItemTTL = remember(groupInfo.id) { mutableStateOf(if (groupInfo.chatItemTTL != null) ChatItemTTL.fromSeconds(groupInfo.chatItemTTL) else null) }
val deletingItems = rememberSaveable(groupInfo.id) { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val activeSortedMembers = remember { chatModel.groupMembers }.value
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
.sortedByDescending { it.memberRole }
Log.e(TAG, "######### GroupChatInfoView chatModel.groupMembers length = ${chatModel.groupMembers.value.count()}")
GroupChatInfoLayout(
chat,
@@ -95,9 +100,7 @@ fun ModalData.GroupChatInfoView(
setChatTTLAlert(chatsCtx, chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems)
},
activeSortedMembers = remember { chatModel.groupMembers }.value
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
.sortedByDescending { it.memberRole },
activeSortedMembers = activeSortedMembers,
developerTools,
onLocalAliasChanged = { setGroupAlias(chat, it, chatModel) },
groupLink,
@@ -140,6 +143,15 @@ fun ModalData.GroupChatInfoView(
addOrEditWelcomeMessage = {
ModalManager.end.showCustomModal { close -> GroupWelcomeView(chatModel, rhId, groupInfo, close) }
},
openMemberSupport = {
ModalManager.end.showCustomModal { close ->
MemberSupportView(
chat,
groupInfo,
close
)
}
},
openPreferences = {
ModalManager.end.showCustomModal { close ->
GroupPreferencesView(
@@ -317,6 +329,30 @@ fun AddGroupMembersButton(
)
}
@Composable
fun UserSupportChatButton(groupInfo: GroupInfo) {
val scope = rememberCoroutineScope()
val scrollToItemId: MutableState<Long?> = remember { mutableStateOf(null) }
SettingsActionItem(
painterResource(MR.images.ic_flag),
stringResource(MR.strings.button_support_chat),
click = {
val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = null)
val supportChatInfo = ChatInfo.Group(groupInfo, groupChatScope = scopeInfo)
scope.launch {
showMemberSupportChatView(
chatModel.chatId,
scrollToItemId = scrollToItemId,
supportChatInfo,
scopeInfo
)
}
},
iconColor = MaterialTheme.colors.secondary,
)
}
@Composable
fun ModalData.GroupChatInfoLayout(
chat: Chat,
@@ -337,6 +373,7 @@ fun ModalData.GroupChatInfoLayout(
showMemberInfo: (GroupMember) -> Unit,
editGroupProfile: () -> Unit,
addOrEditWelcomeMessage: () -> Unit,
openMemberSupport: () -> Unit,
openPreferences: () -> Unit,
deleteGroup: () -> Unit,
clearChat: () -> Unit,
@@ -422,6 +459,29 @@ fun ModalData.GroupChatInfoLayout(
SectionSpacer()
var anyTopSectionRowShow = false
SectionView {
if (groupInfo.membership.supportChat != null) {
anyTopSectionRowShow = true
UserSupportChatButton(groupInfo)
}
if (groupInfo.businessChat == null && groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
anyTopSectionRowShow = true
MemberSupportButton(openMemberSupport)
}
if (groupInfo.canModerate) {
anyTopSectionRowShow = true
GroupReportsButton {
scope.launch {
showGroupReportsView(chatModel.chatId, scrollToItemId, chat.chatInfo)
}
}
}
}
if (anyTopSectionRowShow) {
SectionDividerSpaced(maxBottomPadding = false)
}
SectionView {
if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) {
EditGroupProfileButton(editGroupProfile)
@@ -431,19 +491,17 @@ fun ModalData.GroupChatInfoLayout(
}
val prefsTitleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences
GroupPreferencesButton(prefsTitleId, openPreferences)
if (groupInfo.canModerate) {
GroupReportsButton {
scope.launch {
showGroupReportsView(chatModel.chatId, scrollToItemId, chat.chatInfo)
}
}
}
}
val footerId = if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs
SectionTextFooter(stringResource(footerId))
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
SectionView {
if (activeSortedMembers.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
} else {
SendReceiptsOptionDisabled()
}
WallpaperButton {
ModalManager.end.showModal {
val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } }
@@ -453,12 +511,9 @@ fun ModalData.GroupChatInfoLayout(
}
}
}
ChatTTLOption(chatItemTTL, setChatItemTTL, deletingItems)
SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer))
}
val footerId = if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs
SectionTextFooter(stringResource(footerId))
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems)
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = true)
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) {
@@ -481,7 +536,7 @@ fun ModalData.GroupChatInfoLayout(
}
if (activeSortedMembers.size > 8) {
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
SearchRowView(searchText)
MemberListSearchRowView(searchText)
}
}
SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
@@ -631,17 +686,14 @@ private fun SelectedItemsCounterToolbarSetter(
}
@Composable
fun ChatTTLSection(chatItemTTL: State<ChatItemTTL?>, setChatItemTTL: (ChatItemTTL?) -> Unit, deletingItems: State<Boolean>) {
fun ChatTTLOption(chatItemTTL: State<ChatItemTTL?>, setChatItemTTL: (ChatItemTTL?) -> Unit, deletingItems: State<Boolean>) {
Box {
SectionView {
TtlOptions(
chatItemTTL,
enabled = remember { derivedStateOf { !deletingItems.value } },
onSelected = setChatItemTTL,
default = chatModel.chatItemTTL
)
SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer))
}
TtlOptions(
chatItemTTL,
enabled = remember { derivedStateOf { !deletingItems.value } },
onSelected = setChatItemTTL,
default = chatModel.chatItemTTL
)
if (deletingItems.value) {
Box(Modifier.matchParentSize()) {
ProgressIndicator()
@@ -683,6 +735,15 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo, groupInfo: GroupInfo) {
}
}
@Composable
private fun MemberSupportButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_flag), // TODO [knocking] change icon
stringResource(MR.strings.member_support),
click = onClick
)
}
@Composable
private fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit) {
SettingsActionItem(
@@ -820,7 +881,7 @@ fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = tr
}
@Composable
private fun MemberVerifiedShield() {
fun MemberVerifiedShield() {
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.padding(end = 3.dp).size(16.dp), tint = MaterialTheme.colors.secondary)
}
@@ -941,7 +1002,7 @@ private fun DeleteGroupButton(titleId: StringResource, onClick: () -> Unit) {
}
@Composable
private fun SearchRowView(
fun MemberListSearchRowView(
searchText: MutableState<TextFieldValue> = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
) {
Box(Modifier.width(36.dp), contentAlignment = Alignment.Center) {
@@ -1016,7 +1077,18 @@ fun PreviewGroupChatInfoLayout() {
selectedItems = remember { mutableStateOf(null) },
appBar = remember { mutableStateOf(null) },
scrollToItemId = remember { mutableStateOf(null) },
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {}, deletingItems = remember { mutableStateOf(true) }
addMembers = {},
showMemberInfo = {},
editGroupProfile = {},
addOrEditWelcomeMessage = {},
openMemberSupport = {},
openPreferences = {},
deleteGroup = {},
clearChat = {},
leaveGroup = {},
manageGroupLink = {},
onSearchClicked = {},
deletingItems = remember { mutableStateOf(true) }
)
}
}
@@ -299,6 +299,30 @@ fun GroupMemberInfoLayout(
}
}
@Composable
fun SupportChatButton() {
val scope = rememberCoroutineScope()
val scrollToItemId: MutableState<Long?> = remember { mutableStateOf(null) } // TODO [knocking] scroll to report from support chat?
SettingsActionItem(
painterResource(MR.images.ic_flag),
stringResource(MR.strings.button_support_chat),
click = {
val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = member)
val supportChatInfo = ChatInfo.Group(groupInfo, groupChatScope = scopeInfo)
scope.launch {
showMemberSupportChatView(
chatModel.chatId,
scrollToItemId = scrollToItemId,
supportChatInfo,
scopeInfo
)
}
},
iconColor = MaterialTheme.colors.secondary,
)
}
@Composable
fun ModeratorDestructiveSection() {
val canBlockForAll = member.canBlockForAll(groupInfo)
@@ -413,6 +437,12 @@ fun GroupMemberInfoLayout(
if (member.memberActive) {
SectionView {
// TODO [knocking] allow creating support chat with members that don't have it (currently doesn't work)
// groupInfo.membership.memberRole >= GroupMemberRole.Moderator &&
// (member.memberRole <= GroupMemberRole.Moderator || member.supportChat != null)
if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator && member.supportChat != null) {
SupportChatButton()
}
if (connectionCode != null) {
VerifyCodeButton(member.verified, verifyClicked)
}
@@ -44,7 +44,7 @@ fun SelectedItemsMembersToolbar(
) {
// It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty
Box(Modifier.alpha(0f)) {
ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() })
ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() })
}
Row(
Modifier
@@ -29,6 +29,7 @@ private val MAX_PICKER_HEIGHT = (PICKER_ROW_SIZE * 4) + (MEMBER_ROW_AVATAR_SIZE
@Composable
fun GroupMentions(
chatsCtx: ChatModel.ChatsContext,
rhId: Long?,
composeState: MutableState<ComposeState>,
composeViewFocusRequester: FocusRequester?,
@@ -42,12 +43,31 @@ fun GroupMentions(
val mentionName = remember { mutableStateOf("") }
val mentionRange = remember { mutableStateOf<TextRange?>(null) }
val mentionMemberId = remember { mutableStateOf<String?>(null) }
fun contextMemberFilter(member: GroupMember): Boolean =
when (chatsCtx.secondaryContextFilter) {
null -> true
is SecondaryContextFilter.GroupChatScopeContext ->
when (chatsCtx.secondaryContextFilter.groupScopeInfo) {
is GroupChatScopeInfo.MemberSupport -> {
val scopeMember = chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_
if (scopeMember != null) {
member.memberRole >= GroupMemberRole.Moderator || member.groupMemberId == scopeMember.groupMemberId
} else {
member.memberRole >= GroupMemberRole.Moderator
}
}
}
is SecondaryContextFilter.MsgContentTagContext -> false
}
val filteredMembers = remember {
derivedStateOf {
val members = chatModel.groupMembers.value
.filter {
val status = it.memberStatus
status != GroupMemberStatus.MemLeft && status != GroupMemberStatus.MemRemoved && status != GroupMemberStatus.MemInvited
&& contextMemberFilter(it)
}
.sortedByDescending { it.memberRole }
@@ -17,7 +17,9 @@ import chat.simplex.common.views.usersettings.PreferenceToggleWithIcon
import chat.simplex.common.model.*
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.*
private val featureRoles: List<Pair<GroupMemberRole?, String>> = listOf(
@@ -71,6 +73,16 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () ->
preferences = currentPreferences
},
savePrefs = ::savePrefs,
openMemberAdmission = {
ModalManager.end.showCustomModal { close ->
MemberAdmissionView(
chatModel,
rhId,
chatId,
close
)
}
}
)
}
}
@@ -83,10 +95,15 @@ private fun GroupPreferencesLayout(
applyPrefs: (FullGroupPreferences) -> Unit,
reset: () -> Unit,
savePrefs: () -> Unit,
openMemberAdmission: () -> Unit,
) {
ColumnWithScrollBar {
val titleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences
AppBarTitle(stringResource(titleId))
if (groupInfo.businessChat == null) {
MemberAdmissionButton(openMemberAdmission)
SectionDividerSpaced(maxBottomPadding = false)
}
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) }
val onTTLUpdated = { ttl: Int? ->
applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl)))
@@ -156,6 +173,15 @@ private fun GroupPreferencesLayout(
}
}
@Composable
private fun MemberAdmissionButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_toggle_on),
stringResource(MR.strings.member_admission),
click = onClick
)
}
@Composable
private fun FeatureSection(
feature: GroupFeature,
@@ -53,7 +53,7 @@ fun GroupReportsAppBar(
}
@Composable
private fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) {
fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) {
LaunchedEffect(Unit) {
snapshotFlow { chatModel.chatId.value }
.distinctUntilChanged()
@@ -69,7 +69,7 @@ private fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) {
}
suspend fun showGroupReportsView(staleChatId: State<String?>, scrollToItemId: MutableState<Long?>, chatInfo: ChatInfo) {
val reportsChatsCtx = ChatModel.ChatsContext(contentTag = MsgContentTag.Report)
val reportsChatsCtx = ChatModel.ChatsContext(secondaryContextFilter = SecondaryContextFilter.MsgContentTagContext(MsgContentTag.Report))
openChat(secondaryChatsCtx = reportsChatsCtx, chatModel.remoteHostId(), chatInfo)
ModalManager.end.showCustomModal(true, id = ModalViewId.SECONDARY_CHAT) { close ->
ModalView({}, showAppBar = false) {
@@ -0,0 +1,152 @@
package chat.simplex.common.views.chat.group
import InfoRow
import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView
import SectionTextFooter
import SectionView
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.platform.chatModel
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun MemberAdmissionView(m: ChatModel, rhId: Long?, chatId: String, close: () -> Unit) {
val groupInfo = remember { derivedStateOf {
val ch = m.getChat(chatId)
val g = (ch?.chatInfo as? ChatInfo.Group)?.groupInfo
if (g == null || ch.remoteHostId != rhId) null else g
}}
val gInfo = groupInfo.value ?: return
var admission by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(gInfo.groupProfile.memberAdmission) }
var currentAdmission by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(admission) }
fun saveAdmission(afterSave: () -> Unit = {}) {
withBGApi {
val gp = gInfo.groupProfile.copy(memberAdmission = admission)
val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp)
if (g != null) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateGroup(rhId, g)
currentAdmission = admission
}
}
afterSave()
}
}
ModalView(
close = {
if (admission == currentAdmission) close()
else showUnsavedChangesAlert({ saveAdmission(close) }, close)
},
) {
MemberAdmissionLayout(
admission,
currentAdmission,
gInfo,
applyAdmission = { admsn ->
admission = admsn
},
reset = {
admission = currentAdmission
},
saveAdmission = ::saveAdmission,
)
}
}
@Composable
private fun MemberAdmissionLayout(
admission: GroupMemberAdmission?,
currentAdmission: GroupMemberAdmission?,
groupInfo: GroupInfo,
applyAdmission: (GroupMemberAdmission) -> Unit,
reset: () -> Unit,
saveAdmission: () -> Unit,
) {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.member_admission))
val review = remember(admission) { mutableStateOf(admission?.review) }
AdmissionSection(MR.strings.admission_stage_review, MR.strings.admission_stage_review_descr, review, groupInfo) { criteria ->
if (admission != null) {
applyAdmission(admission.copy(review = criteria))
} else {
applyAdmission(GroupMemberAdmission(review = criteria))
}
}
if (groupInfo.isOwner) {
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
ResetSaveButtons(
reset = reset,
save = saveAdmission,
disabled = admission == currentAdmission
)
}
SectionBottomSpacer()
}
}
private val memberCriterias: List<Pair<MemberCriteria?, String>> = listOf(
null to generalGetString(MR.strings.member_criteria_off),
MemberCriteria.All to generalGetString(MR.strings.member_criteria_all)
)
@Composable
private fun AdmissionSection(
admissionStageStrId: StringResource,
admissionStageDescrStrId: StringResource,
memberCriteria: State<MemberCriteria?>,
groupInfo: GroupInfo,
onSelected: (MemberCriteria?) -> Unit
) {
SectionView {
if (groupInfo.isOwner) {
ExposedDropDownSettingRow(
generalGetString(admissionStageStrId),
memberCriterias,
memberCriteria,
onSelected = { value ->
onSelected(value)
}
)
} else {
InfoRow(
stringResource(admissionStageStrId),
memberCriteria.value?.text ?: generalGetString(MR.strings.member_criteria_off)
)
}
}
SectionTextFooter(stringResource( admissionStageDescrStrId))
}
@Composable
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
SectionView {
SectionItemView(reset, disabled = disabled) {
Text(stringResource(MR.strings.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
SectionItemView(save, disabled = disabled) {
Text(stringResource(MR.strings.save_and_notify_group_members), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.save_admission_question),
confirmText = generalGetString(MR.strings.save_and_notify_group_members),
dismissText = generalGetString(MR.strings.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}
@@ -0,0 +1,122 @@
package chat.simplex.common.views.chat.group
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@Composable
private fun MemberSupportChatView(memberSupportChatsCtx: ChatModel.ChatsContext, staleChatId: State<String?>, scrollToItemId: MutableState<Long?>) {
ChatView(memberSupportChatsCtx, staleChatId, scrollToItemId, onComposed = {})
}
@Composable
fun MemberSupportChatAppBar(
chatsCtx: ChatModel.ChatsContext,
scopeMember_: GroupMember?,
close: () -> Unit,
onSearchValueChanged: (String) -> Unit
) {
val oneHandUI = remember { ChatController.appPrefs.oneHandUI.state }
val showSearch = rememberSaveable { mutableStateOf(false) }
val onBackClicked = {
if (!showSearch.value) {
close()
} else {
onSearchValueChanged("")
showSearch.value = false
}
}
BackHandler(onBack = onBackClicked)
if (scopeMember_ != null) {
DefaultAppBar(
navigationButton = { NavigationButtonBack(onBackClicked) },
title = { MemberSupportChatToolbarTitle(scopeMember_) },
onTitleClick = null,
onTop = !oneHandUI.value,
showSearch = showSearch.value,
onSearchValueChanged = onSearchValueChanged,
buttons = {
IconButton({ showSearch.value = true }) {
Icon(painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary)
}
}
)
} else {
DefaultAppBar(
navigationButton = { NavigationButtonBack(onBackClicked) },
fixedTitleText = stringResource(MR.strings.support_chat),
onTitleClick = null,
onTop = !oneHandUI.value,
showSearch = showSearch.value,
onSearchValueChanged = onSearchValueChanged,
buttons = {
IconButton({ showSearch.value = true }) {
Icon(painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary)
}
}
)
}
ItemsReload(chatsCtx)
}
@Composable
fun MemberSupportChatToolbarTitle(member: GroupMember, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
MemberProfileImage(size = imageSize * fontSizeSqrtMultiplier, member, iconColor)
Column(
Modifier.padding(start = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (member.verified) {
MemberVerifiedShield()
}
Text(
member.displayName, fontWeight = FontWeight.SemiBold,
maxLines = 1, overflow = TextOverflow.Ellipsis
)
}
if (member.fullName != "" && member.fullName != member.displayName && member.localAlias.isEmpty()) {
Text(
member.fullName,
maxLines = 1, overflow = TextOverflow.Ellipsis
)
}
}
}
}
suspend fun showMemberSupportChatView(staleChatId: State<String?>, scrollToItemId: MutableState<Long?>, chatInfo: ChatInfo, scopeInfo: GroupChatScopeInfo) {
val memberSupportChatsCtx = ChatModel.ChatsContext(secondaryContextFilter = SecondaryContextFilter.GroupChatScopeContext(scopeInfo))
openChat(secondaryChatsCtx = memberSupportChatsCtx, chatModel.remoteHostId(), chatInfo)
ModalManager.end.showCustomModal(true, id = ModalViewId.SECONDARY_CHAT) { close ->
ModalView({}, showAppBar = false) {
if (chatInfo is ChatInfo.Group && chatInfo.groupChatScope != null) {
MemberSupportChatView(memberSupportChatsCtx, staleChatId, scrollToItemId)
} else {
LaunchedEffect(Unit) {
close()
}
}
}
}
}
@@ -0,0 +1,260 @@
package chat.simplex.common.views.chat.group
import SectionBottomSpacer
import SectionItemView
import SectionItemViewLongClickable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chatlist.unreadCountStr
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.launch
@Composable
fun ModalData.MemberSupportView(
chat: Chat,
groupInfo: GroupInfo,
close: () -> Unit
) {
KeyChangeEffect(chat.id) {
close()
}
ModalView(close = close) {
MemberSupportViewLayout(
chat,
groupInfo
)
}
}
@Composable
private fun ModalData.MemberSupportViewLayout(
chat: Chat,
groupInfo: GroupInfo
) {
val oneHandUI = remember { ChatController.appPrefs.oneHandUI.state }
val scope = rememberCoroutineScope()
val scrollToItemId: MutableState<Long?> = remember { mutableStateOf(null) } // TODO [knocking] scroll to report from support chat?
val membersWithChats = remember { chatModel.groupMembers }.value
.filter { it.supportChat != null && it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
.sortedWith(
compareByDescending<GroupMember> { it.memberPending }
.thenByDescending { (it.supportChat?.mentions ?: 0) > 0 }
.thenByDescending { (it.supportChat?.memberAttention ?: 0) > 0 }
.thenByDescending { (it.supportChat?.unread ?: 0) > 0 }
.thenByDescending { it.supportChat?.chatTs }
)
val searchText = remember { stateGetOrPut("searchText") { TextFieldValue() } }
val filteredmembersWithChats = remember(membersWithChats) {
derivedStateOf {
val s = searchText.value.text.trim().lowercase()
if (s.isEmpty()) membersWithChats else membersWithChats.filter { m -> m.anyNameContains(s) }
}
}
LazyColumnWithScrollBar(
contentPadding =
PaddingValues(
top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp else topPaddingToContent(false)
)
) {
item {
AppBarTitle(stringResource(MR.strings.member_support))
}
if (membersWithChats.isEmpty()) {
item {
Box(Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING), contentAlignment = Alignment.Center) {
Text(generalGetString(MR.strings.no_support_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
}
}
} else {
item {
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
MemberListSearchRowView(searchText)
}
}
items(filteredmembersWithChats.value, key = { it.groupMemberId }) { member ->
Divider()
val showMenu = remember { mutableStateOf(false) }
SectionItemViewLongClickable(
click = {
val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = member)
val supportChatInfo = ChatInfo.Group(groupInfo, groupChatScope = scopeInfo)
scope.launch {
showMemberSupportChatView(
chatModel.chatId,
scrollToItemId = scrollToItemId,
supportChatInfo,
scopeInfo
)
}
},
longClick = { showMenu.value = true },
minHeight = 54.dp,
padding = PaddingValues(horizontal = DEFAULT_PADDING)
) {
Box(contentAlignment = Alignment.CenterStart) {
DropDownMenuForSupportChat(chat.remoteHostId, member, groupInfo, showMenu)
SupportChatRow(member)
}
}
}
item {
Divider()
SectionBottomSpacer()
}
}
}
}
@Composable
fun SupportChatRow(member: GroupMember) {
fun memberStatus(): String {
return if (member.activeConn?.connDisabled == true) {
generalGetString(MR.strings.member_info_member_disabled)
} else if (member.activeConn?.connDisabled == true) {
generalGetString(MR.strings.member_info_member_inactive)
} else if (member.memberPending) {
member.memberStatus.text
} else {
member.memberRole.text
}
}
@Composable
fun SupportChatUnreadIndicator(supportChat: GroupSupportChat) {
Box(Modifier.widthIn(min = 34.sp.toDp()), contentAlignment = Alignment.TopEnd) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.sp.toDp())) {
if (supportChat.unread > 0 || supportChat.mentions > 0 || supportChat.memberAttention > 0) {
val indicatorTint = when {
supportChat.mentions > 0 || supportChat.memberAttention > 0 -> MaterialTheme.colors.primaryVariant
else -> MaterialTheme.colors.secondary
}
if (supportChat.mentions == 1 && supportChat.unread == 1) {
Box(modifier = Modifier.offset(y = 2.sp.toDp()).size(15.sp.toDp()).background(indicatorTint, shape = CircleShape), contentAlignment = Alignment.Center) {
Icon(
painterResource(MR.images.ic_alternate_email),
contentDescription = generalGetString(MR.strings.notifications),
tint = Color.White,
modifier = Modifier.size(9.sp.toDp())
)
}
} else {
if (supportChat.mentions > 0 && supportChat.unread > 1) {
Icon(
painterResource(MR.images.ic_alternate_email),
contentDescription = generalGetString(MR.strings.notifications),
tint = indicatorTint,
modifier = Modifier.size(12.sp.toDp()).offset(y = 3.sp.toDp())
)
}
Text(
unreadCountStr(supportChat.unread),
color = Color.White,
fontSize = 10.sp,
style = TextStyle(textAlign = TextAlign.Center),
modifier = Modifier
.offset(y = 3.sp.toDp())
.background(indicatorTint, shape = CircleShape)
.badgeLayout()
.padding(horizontal = 2.sp.toDp())
.padding(vertical = 1.sp.toDp())
)
}
}
}
}
}
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
Modifier.weight(1f).padding(top = MEMBER_ROW_VERTICAL_PADDING, end = DEFAULT_PADDING, bottom = MEMBER_ROW_VERTICAL_PADDING),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
MemberProfileImage(size = MEMBER_ROW_AVATAR_SIZE, member)
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
if (member.verified) {
MemberVerifiedShield()
}
Text(
member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
color = if (member.memberIncognito) Indigo else Color.Unspecified
)
}
Text(
memberStatus(),
color = MaterialTheme.colors.secondary,
fontSize = 12.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
Row {
if (member.memberPending) {
Icon(
painterResource(MR.images.ic_flag_filled),
contentDescription = null,
Modifier.padding(end = 3.dp).size(16.dp),
tint = MaterialTheme.colors.primaryVariant
)
}
if (member.supportChat != null) {
SupportChatUnreadIndicator(member.supportChat)
}
}
}
}
@Composable
private fun DropDownMenuForSupportChat(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState<Boolean>) {
DefaultDropdownMenu(showMenu) {
if (member.memberPending) {
ItemAction(stringResource(MR.strings.accept_pending_member_button), painterResource(MR.images.ic_check), color = MaterialTheme.colors.primary, onClick = {
acceptMemberDialog(rhId, groupInfo, member)
showMenu.value = false
})
}
ItemAction(stringResource(MR.strings.remove_pending_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
removeMemberDialog(rhId, groupInfo, member, chatModel)
showMenu.value = false
})
// TODO [knocking] mark read, mark unread
// ItemAction(stringResource(MR.strings.mark_unread), painterResource(MR.images.ic_mark_chat_unread), onClick = {
// showMenu.value = false
// })
}
}
@@ -271,6 +271,7 @@ fun ChatItemView(
}
}
// improvement could be to track "forwarded from" scope and open it
@Composable
fun GoToItemButton(alignStart: Boolean, parentActivated: State<Boolean>) {
val chatTypeApiIdMsgId = cItem.meta.itemForwarded?.chatTypeApiIdMsgId
@@ -1422,7 +1423,7 @@ fun PreviewChatItemView(
chatItem: ChatItem = ChatItem.getSampleData(1, CIDirection.DirectSnd(), Clock.System.now(), "hello")
) {
ChatItemView(
chatsCtx = ChatModel.ChatsContext(contentTag = null),
chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null),
rhId = null,
ChatInfo.Direct.sampleData,
chatItem,
@@ -1472,7 +1473,7 @@ fun PreviewChatItemView(
fun PreviewChatItemViewDeletedContent() {
SimpleXTheme {
ChatItemView(
chatsCtx = ChatModel.ChatsContext(contentTag = null),
chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null),
rhId = null,
ChatInfo.Direct.sampleData,
ChatItem.getDeletedContentSampleData(),
@@ -615,7 +615,8 @@ fun markChatRead(c: Chat) {
chatModel.controller.apiChatRead(
chat.remoteHostId,
chat.chatInfo.chatType,
chat.chatInfo.apiId
chat.chatInfo.apiId,
chat.chatInfo.groupChatScope()
)
chat = chatModel.getChat(chat.id) ?: return@withApi
}
@@ -651,7 +652,7 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
if (success) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
chatModel.chatsContext.updateChatTagReadNoContentTag(chat, wasUnread)
chatModel.chatsContext.updateChatTagReadInPrimaryContext(chat, wasUnread)
}
}
}
@@ -886,7 +887,7 @@ fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: Ch
ChatInfo.Direct(contact.copy(chatSettings = chatSettings))
}
is ChatInfo.Group -> with(chatInfo) {
ChatInfo.Group(groupInfo.copy(chatSettings = chatSettings))
ChatInfo.Group(groupInfo.copy(chatSettings = chatSettings), groupChatScope = null)
}
else -> null
}
@@ -914,7 +915,7 @@ fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: Ch
val updatedChat = chatModel.getChat(chatInfo.id)
if (updatedChat != null) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateChatTagReadNoContentTag(updatedChat, wasUnread)
chatModel.chatsContext.updateChatTagReadInPrimaryContext(updatedChat, wasUnread)
}
}
val current = currentState?.value
@@ -158,6 +158,7 @@
<string name="error_loading_details">Error loading details</string>
<string name="error_adding_members">Error adding member(s)</string>
<string name="error_joining_group">Error joining group</string>
<string name="error_accepting_member">Error accepting member</string>
<string name="cannot_receive_file">Cannot receive file</string>
<string name="sender_cancelled_file_transfer">Sender cancelled file transfer.</string>
<string name="file_not_approved_title">Unknown servers!</string>
@@ -487,6 +488,7 @@
<string name="image_decoding_exception_desc">The image cannot be decoded. Please, try a different image or contact developers.</string>
<string name="video_decoding_exception_desc">The video cannot be decoded. Please, try a different video or contact developers.</string>
<string name="you_are_observer">you are observer</string>
<string name="reviewed_by_moderators">reviewed by moderators</string>
<string name="observer_cant_send_message_title">You can\'t send messages!</string>
<string name="observer_cant_send_message_desc">Please contact group admin.</string>
<string name="files_and_media_prohibited">Files and media prohibited!</string>
@@ -1048,6 +1050,7 @@
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</string>
<string name="edit_image">Edit image</string>
<string name="delete_image">Delete image</string>
<string name="save_admission_question">Save admission settings?</string>
<string name="save_preferences_question">Save preferences?</string>
<string name="save_and_notify_contact">Save and notify contact</string>
<string name="save_and_notify_contacts">Save and notify contacts</string>
@@ -1567,6 +1570,8 @@
<!-- Group event chat items -->
<string name="rcv_group_event_member_added">invited %1$s</string>
<string name="rcv_group_event_member_connected">connected</string>
<string name="rcv_group_event_member_accepted">accepted %1$s</string>
<string name="rcv_group_event_user_accepted">accepted you</string>
<string name="rcv_group_event_member_left">left</string>
<string name="rcv_group_event_changed_member_role">changed role of %s to %s</string>
<string name="rcv_group_event_member_blocked">blocked %s</string>
@@ -1644,6 +1649,8 @@
<string name="group_member_status_invited">invited</string>
<string name="group_member_status_pending_approval">pending approval</string>
<string name="group_member_status_pending_approval_short">pending</string>
<string name="group_member_status_pending_review">pending review</string>
<string name="group_member_status_pending_review_short">review</string>
<string name="group_member_status_introduced">connecting (introduced)</string>
<string name="group_member_status_intro_invitation">connecting (introduction invitation)</string>
<string name="group_member_status_accepted">connecting (accepted)</string>
@@ -1750,7 +1757,7 @@
<string name="button_remove_member_question">Remove member?</string>
<string name="button_remove_members_question">Remove members?</string>
<string name="button_remove_member">Remove member</string>
<string name="button_support_chat">Support chat</string>
<string name="button_send_direct_message">Send direct message</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">Member will be removed from group - this cannot be undone!</string>
<string name="members_will_be_removed_from_group_cannot_be_undone">Members will be removed from group - this cannot be undone!</string>
@@ -2144,6 +2151,26 @@
<string name="feature_roles_owners">owners</string>
<string name="feature_enabled_for">Enabled for</string>
<!-- MemberAdmission.kt -->
<string name="member_admission">Member admission</string>
<string name="admission_stage_review">Review</string>
<string name="admission_stage_review_descr">Review new members before admitting to group.</string>
<string name="member_criteria_off">off</string>
<string name="member_criteria_all">all</string>
<!-- MemberSupportView.kt -->
<string name="member_support">Member support</string>
<string name="no_support_chats">No support chats</string>
<!-- MemberSupportChatView.kt -->
<string name="support_chat">Support</string>
<string name="remove_pending_member_button">Remove</string>
<string name="accept_pending_member_button">Accept</string>
<string name="accept_pending_member_alert_title">Accept member</string>
<string name="accept_pending_member_alert_question">Member will join the group, accept member?</string>
<string name="accept_pending_member_alert_confirmation_as_member">Accept as member</string>
<string name="accept_pending_member_alert_confirmation_as_observer">Accept as observer</string>
<!-- WhatsNewView.kt -->
<string name="whats_new">What\'s new</string>
<string name="new_in_version">New in %s</string>
@@ -48,6 +48,7 @@ actual fun PlatformTextField(
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
userIsPending: Boolean,
placeholder: String,
showVoiceButton: Boolean,
onMessageChange: (ComposeMessage) -> Unit,
@@ -204,6 +205,8 @@ actual fun PlatformTextField(
showDeleteTextButton.value = cs.message.text.split("\n").size >= 4 && !cs.inProgress
if (composeState.value.preview is ComposePreview.VoicePreview) {
ComposeOverlay(MR.strings.voice_message_send_text, textStyle, padding)
} else if (userIsPending) {
ComposeOverlay(MR.strings.reviewed_by_moderators, textStyle, padding)
} else if (userIsObserver) {
ComposeOverlay(MR.strings.you_are_observer, textStyle, padding)
}
+3 -3
View File
@@ -1578,7 +1578,7 @@ processChatCommand' vr = \case
case memberConnId m of
Just connId -> do
cStats@ConnectionStats {ratchetSyncState = rss} <- withAgent $ \a -> synchronizeRatchet a connId PQSupportOff force
(g', m', scopeInfo) <- liftIO $ mkGroupChatScope g m
(g', m', scopeInfo) <- mkGroupChatScope g m
createInternalChatItem user (CDGroupSnd g' scopeInfo) (CISndConnEvent . SCERatchetSync rss . Just $ groupMemberRef m') Nothing
pure $ CRGroupMemberRatchetSyncStarted user g' m' cStats
_ -> throwChatError CEGroupMemberNotActive
@@ -2244,7 +2244,7 @@ processChatCommand' vr = \case
pure m {memberStatus = GSMemRemoved}
deletePendingMember :: ([ChatError], [GroupMember], [AChatItem]) -> User -> GroupInfo -> [GroupMember] -> GroupMember -> CM ([ChatError], [GroupMember], [AChatItem])
deletePendingMember (accErrs, accDeleted, accACIs) user gInfo recipients m = do
(m', scopeInfo) <- liftIO $ mkMemberSupportChatInfo m
(m', scopeInfo) <- mkMemberSupportChatInfo m
(errs, deleted, acis) <- deleteMemsSend user gInfo (Just scopeInfo) recipients [m']
pure (errs <> accErrs, deleted <> accDeleted, acis <> accACIs)
deleteMemsSend :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> [GroupMember] -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem])
@@ -2280,7 +2280,7 @@ processChatCommand' vr = \case
cancelFilesInProgress user filesInfo
let recipients = filter memberCurrentOrPending members
msg <- sendGroupMessage' user gInfo recipients XGrpLeave
(gInfo', scopeInfo) <- liftIO $ mkLocalGroupChatScope gInfo
(gInfo', scopeInfo) <- mkLocalGroupChatScope gInfo
ci <- saveSndChatItem user (CDGroupSnd gInfo' scopeInfo) msg (CISndGroupEvent SGEUserLeft)
toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo' scopeInfo) ci]
-- TODO delete direct connections that were unused
+14 -12
View File
@@ -1323,7 +1323,7 @@ getGroupRecipients vr user gInfo@GroupInfo {membership} scope modsCompatVersion
compatible GroupMember {activeConn, memberChatVRange} =
maxVersion (maybe memberChatVRange peerChatVRange activeConn) >= modsCompatVersion
mkLocalGroupChatScope :: GroupInfo -> IO (GroupInfo, Maybe GroupChatScopeInfo)
mkLocalGroupChatScope :: GroupInfo -> CM (GroupInfo, Maybe GroupChatScopeInfo)
mkLocalGroupChatScope gInfo@GroupInfo {membership}
| memberPending membership = do
(gInfo', scopeInfo) <- mkGroupSupportChatInfo gInfo
@@ -1331,7 +1331,7 @@ mkLocalGroupChatScope gInfo@GroupInfo {membership}
| otherwise =
pure (gInfo, Nothing)
mkGroupChatScope :: GroupInfo -> GroupMember -> IO (GroupInfo, GroupMember, Maybe GroupChatScopeInfo)
mkGroupChatScope :: GroupInfo -> GroupMember -> CM (GroupInfo, GroupMember, Maybe GroupChatScopeInfo)
mkGroupChatScope gInfo@GroupInfo {membership} m
| memberPending membership = do
(gInfo', scopeInfo) <- mkGroupSupportChatInfo gInfo
@@ -1344,38 +1344,40 @@ mkGroupChatScope gInfo@GroupInfo {membership} m
mkGetMessageChatScope :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> Maybe MsgScope -> CM (GroupInfo, GroupMember, Maybe GroupChatScopeInfo)
mkGetMessageChatScope vr user gInfo@GroupInfo {membership} m msgScope_ =
liftIO (mkGroupChatScope gInfo m) >>= \case
mkGroupChatScope gInfo m >>= \case
groupScope@(_gInfo', _m', Just _scopeInfo) -> pure groupScope
(_, _, Nothing) -> case msgScope_ of
Nothing -> pure (gInfo, m, Nothing)
Just (MSMember mId)
| sameMemberId mId membership -> do
(gInfo', scopeInfo) <- liftIO $ mkGroupSupportChatInfo gInfo
(gInfo', scopeInfo) <- mkGroupSupportChatInfo gInfo
pure (gInfo', m, Just scopeInfo)
| otherwise -> do
referredMember <- withStore $ \db -> getGroupMemberByMemberId db vr user gInfo mId
-- TODO [knocking] return patched _referredMember' too?
(_referredMember', scopeInfo) <- liftIO $ mkMemberSupportChatInfo referredMember
(_referredMember', scopeInfo) <- mkMemberSupportChatInfo referredMember
pure (gInfo, m, Just scopeInfo)
mkGroupSupportChatInfo :: GroupInfo -> IO (GroupInfo, GroupChatScopeInfo)
mkGroupSupportChatInfo :: GroupInfo -> CM (GroupInfo, GroupChatScopeInfo)
mkGroupSupportChatInfo gInfo@GroupInfo {membership} =
case supportChat membership of
Nothing -> do
chatTs <- getCurrentTime
let gInfo' = gInfo {membership = membership {supportChat = Just $ GroupSupportChat chatTs 1 0 0}}
chatTs <- liftIO getCurrentTime
withStore' $ \db -> setSupportChatTs db (groupMemberId' membership) chatTs
let gInfo' = gInfo {membership = membership {supportChat = Just $ GroupSupportChat chatTs 0 0 0}}
scopeInfo = GCSIMemberSupport {groupMember_ = Nothing}
pure (gInfo', scopeInfo)
Just _supportChat ->
let scopeInfo = GCSIMemberSupport {groupMember_ = Nothing}
in pure (gInfo, scopeInfo)
mkMemberSupportChatInfo :: GroupMember -> IO (GroupMember, GroupChatScopeInfo)
mkMemberSupportChatInfo m@GroupMember {supportChat} =
mkMemberSupportChatInfo :: GroupMember -> CM (GroupMember, GroupChatScopeInfo)
mkMemberSupportChatInfo m@GroupMember {groupMemberId, supportChat} =
case supportChat of
Nothing -> do
chatTs <- getCurrentTime
let m' = m {supportChat = Just $ GroupSupportChat chatTs 1 0 0}
chatTs <- liftIO getCurrentTime
withStore' $ \db -> setSupportChatTs db groupMemberId chatTs
let m' = m {supportChat = Just $ GroupSupportChat chatTs 0 0 0}
scopeInfo = GCSIMemberSupport {groupMember_ = Just m'}
pure (m', scopeInfo)
Just _supportChat ->
+18 -18
View File
@@ -777,7 +777,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
else pure $ memberStatus membership
pure (GSMemConnected, membershipStatus)
toView $ CRUserJoinedGroup user gInfo {membership = membership {memberStatus = membershipStatus}} m {memberStatus = mStatus}
(gInfo', m', scopeInfo) <- liftIO $ mkGroupChatScope gInfo m
(gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m
let cd = CDGroupRcv gInfo' scopeInfo m'
createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing
createGroupFeatureItems user cd CIRcvGroupFeature gInfo'
@@ -788,7 +788,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
if not (memberPending m)
then withStore' $ \db -> updateGroupMemberStatus db userId m GSMemConnected $> GSMemConnected
else pure $ memberStatus m
(gInfo', m', scopeInfo) <- liftIO $ mkGroupChatScope gInfo m
(gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m
memberConnectedChatItem gInfo' scopeInfo m'
toView $ CRJoinedGroupMember user gInfo' m' {memberStatus = mStatus}
let Connection {viaUserContactLink} = conn
@@ -843,7 +843,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
tags <- newTVarIO []
withAckMessage "group msg" agentConnId msgMeta True (Just tags) $ \eInfo -> do
-- possible improvement is to choose scope based on event (some events specify scope)
(gInfo', m', scopeInfo) <- liftIO $ mkGroupChatScope gInfo m
(gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m
checkIntegrityCreateItem (CDGroupRcv gInfo' scopeInfo m') msgMeta `catchChatError` \_ -> pure ()
forM_ aChatMsgs $ \case
Right (ACMsg _ chatMsg) ->
@@ -936,12 +936,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
when continued $ sendPendingGroupMessages user m conn
SWITCH qd phase cStats -> do
toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats)
(gInfo', m', scopeInfo) <- liftIO $ mkGroupChatScope gInfo m
(gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m
when (phase == SPStarted || phase == SPCompleted) $ case qd of
QDRcv -> createInternalChatItem user (CDGroupSnd gInfo' scopeInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m') Nothing
QDSnd -> createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m') (CIRcvConnEvent $ RCESwitchQueue phase) Nothing
RSYNC rss cryptoErr_ cStats -> do
(gInfo', m', scopeInfo) <- liftIO $ mkGroupChatScope gInfo m
(gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m
case (rss, connectionCode, cryptoErr_) of
(RSRequired, _, Just cryptoErr) -> processErr gInfo' scopeInfo m' cryptoErr
(RSAllowed, _, Just cryptoErr) -> processErr gInfo' scopeInfo m' cryptoErr
@@ -1234,7 +1234,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
| otherwise -> do
let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo
mem <- acceptGroupJoinRequestAsync user gInfo cReq acceptance useRole profileMode
(gInfo', mem', scopeInfo) <- liftIO $ mkGroupChatScope gInfo mem
(gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem
createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing
toView $ CRAcceptingGroupJoinRequestMember user gInfo' mem'
Left rjctReason
@@ -1379,7 +1379,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
notifyMemberConnected :: GroupInfo -> GroupMember -> Maybe Contact -> CM ()
notifyMemberConnected gInfo m ct_ = do
(gInfo', m', scopeInfo) <- liftIO $ mkGroupChatScope gInfo m
(gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m
memberConnectedChatItem gInfo' scopeInfo m'
lift $ mapM_ (`setContactNetworkStatus` NSConnected) ct_
toView $ CRConnectedToGroupMember user gInfo' m' ct_
@@ -2165,7 +2165,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
BCCustomer -> customerId == memberId
createProfileUpdatedItem m' =
when createItems $ do
(gInfo', m'', scopeInfo) <- liftIO $ mkGroupChatScope gInfo m'
(gInfo', m'', scopeInfo) <- mkGroupChatScope gInfo m'
let ciContent = CIRcvGroupEvent $ RGEMemberProfileUpdated (fromLocalProfile p) p'
createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m'') ciContent itemTs_
@@ -2458,7 +2458,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
Just (MSMember _) -> GSMemPendingReview
_ -> GSMemAnnounced
memberAnnouncedToView announcedMember@GroupMember {groupMemberId, memberProfile} = do
(announcedMember', scopeInfo) <- liftIO $ getMemNewChatScope announcedMember
(announcedMember', scopeInfo) <- getMemNewChatScope announcedMember
let event = RGEMemberAdded groupMemberId (fromLocalProfile memberProfile)
ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo scopeInfo m) msg brokerTs (CIRcvGroupEvent event)
groupMsgToView gInfo scopeInfo ci
@@ -2555,7 +2555,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
| senderRole < GRAdmin || senderRole < fromRole = messageError "x.grp.mem.role with insufficient member permissions"
| otherwise = do
withStore' $ \db -> updateGroupMemberRole db user member memRole
(gInfo'', m', scopeInfo) <- liftIO $ mkGroupChatScope gInfo' m
(gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo' m
ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo'' scopeInfo m') msg brokerTs (CIRcvGroupEvent gEvent)
groupMsgToView gInfo'' scopeInfo ci
toView CRMemberRole {user, groupInfo = gInfo'', byMember = m', member = member {memberRole = memRole}, fromRole, toRole = memRole}
@@ -2584,7 +2584,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
bm' <- setMemberBlocked bm
toggleNtf user bm' (not blocked)
let ciContent = CIRcvGroupEvent $ RGEMemberBlocked bmId (fromLocalProfile bmp) blocked
(gInfo', m', scopeInfo) <- liftIO $ mkGroupChatScope gInfo m
(gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m
ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo' scopeInfo m') msg brokerTs ciContent
groupMsgToView gInfo' scopeInfo ci
toView CRMemberBlockedForAll {user, groupInfo = gInfo', byMember = m', member = bm, blocked}
@@ -2669,7 +2669,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
messageError "x.grp.mem.del with insufficient member permissions"
| otherwise = a
deleteMemberItem gEvent = do
(gInfo', m', scopeInfo) <- liftIO $ mkGroupChatScope gInfo m
(gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m
ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo' scopeInfo m') msg brokerTs (CIRcvGroupEvent gEvent)
groupMsgToView gInfo' scopeInfo ci
deleteMessages :: MsgDirectionI d => GroupMember -> SMsgDirection d -> CM ()
@@ -2682,7 +2682,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
deleteMemberConnection user m
-- member record is not deleted to allow creation of "member left" chat item
withStore' $ \db -> updateGroupMemberStatus db userId m GSMemLeft
(gInfo', m', scopeInfo) <- liftIO $ mkGroupChatScope gInfo m
(gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m
ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo' scopeInfo m') msg brokerTs (CIRcvGroupEvent RGEMemberLeft)
groupMsgToView gInfo' scopeInfo ci
toView $ CRLeftMember user gInfo' m' {memberStatus = GSMemLeft}
@@ -2696,7 +2696,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
pure members
-- member records are not deleted to keep history
deleteMembersConnections user ms
(gInfo'', m', scopeInfo) <- liftIO $ mkGroupChatScope gInfo m
(gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo m
ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo'' scopeInfo m') msg brokerTs (CIRcvGroupEvent RGEGroupDeleted)
groupMsgToView gInfo'' scopeInfo ci
toView $ CRGroupDeleted user gInfo'' {membership = membership {memberStatus = GSMemGroupDeleted}} m'
@@ -2707,7 +2707,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
| otherwise = case businessChat of
Nothing -> unless (p == p') $ do
g' <- withStore $ \db -> updateGroupProfile db user g p'
(g'', m', scopeInfo) <- liftIO $ mkGroupChatScope g' m
(g'', m', scopeInfo) <- mkGroupChatScope g' m
toView $ CRGroupUpdated user g g'' (Just m')
let cd = CDGroupRcv g'' scopeInfo m'
unless (sameGroupProfileInfo p p') $ do
@@ -2726,7 +2726,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
unless (groupPreferences p == Just ps') $ do
g' <- withStore' $ \db -> updateGroupPreferences db user g ps'
toView $ CRGroupUpdated user g g' (Just m)
(g'', m', scopeInfo) <- liftIO $ mkGroupChatScope g' m
(g'', m', scopeInfo) <- mkGroupChatScope g' m
let cd = CDGroupRcv g'' scopeInfo m'
createGroupFeatureChangedItems user cd CIRcvGroupFeature g g''
@@ -2768,7 +2768,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
dm <- encodeConnInfo $ XInfo p
joinAgentConnectionAsync user True connReq dm subMode
createItems mCt' m' = do
(g', m'', scopeInfo) <- liftIO $ mkGroupChatScope g m'
(g', m'', scopeInfo) <- mkGroupChatScope g m'
createInternalChatItem user (CDGroupRcv g' scopeInfo m'') (CIRcvGroupEvent RGEMemberCreatedContact) Nothing
toView $ CRNewMemberContactReceivedInv user mCt' g' m''
forM_ mContent_ $ \mc -> do
@@ -2827,7 +2827,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
groupMsgReceived :: GroupInfo -> GroupMember -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> CM ()
groupMsgReceived gInfo m conn@Connection {connId} msgMeta msgRcpts = do
(gInfo', m', scopeInfo) <- liftIO $ mkGroupChatScope gInfo m
(gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m
checkIntegrityCreateItem (CDGroupRcv gInfo' scopeInfo m') msgMeta `catchChatError` \_ -> pure ()
forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do
withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus
+9 -9
View File
@@ -321,7 +321,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr = do
-- | creates completely new group with a single member - the current user
createNewGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> ExceptT StoreError IO GroupInfo
createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = ExceptT $ do
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
let GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} = groupProfile
fullGroupPreferences = mergeGroupPreferences groupPreferences
currentTs <- getCurrentTime
customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile
@@ -329,8 +329,8 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc
groupId <- liftIO $ do
DB.execute
db
"INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
(displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs)
"INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
(displayName, fullName, description, image, userId, groupPreferences, memberAdmission, currentTs, currentTs)
profileId <- insertedRowId db
DB.execute
db
@@ -390,7 +390,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ
DB.query db "SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMIT 1" (connRequest, userId)
createGroupInvitation_ :: ExceptT StoreError IO (GroupInfo, GroupMemberId)
createGroupInvitation_ = do
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
let GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} = groupProfile
fullGroupPreferences = mergeGroupPreferences groupPreferences
ExceptT $
withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do
@@ -398,8 +398,8 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ
groupId <- liftIO $ do
DB.execute
db
"INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
(displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs)
"INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
(displayName, fullName, description, image, userId, groupPreferences, memberAdmission, currentTs, currentTs)
profileId <- insertedRowId db
DB.execute
db
@@ -558,13 +558,13 @@ createGroupViaLink'
(,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user hostMemberId
where
insertGroup_ currentTs = ExceptT $ do
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
let GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} = groupProfile
withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do
liftIO $ do
DB.execute
db
"INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
(displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs)
"INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
(displayName, fullName, description, image, userId, groupPreferences, memberAdmission, currentTs, currentTs)
profileId <- insertedRowId db
DB.execute
db
+27 -14
View File
@@ -37,6 +37,7 @@ module Simplex.Chat.Store.Messages
deleteOldMessages,
MemberAttention (..),
updateChatTsStats,
setSupportChatTs,
createNewSndChatItem,
createNewRcvChatItem,
createNewChatItemNoMsg,
@@ -419,6 +420,10 @@ updateChatTsStats db User {userId} chatDirection chatTs chatStats_ = case toChat
(chatTs, userId, noteFolderId)
_ -> pure ()
setSupportChatTs :: DB.Connection -> GroupMemberId -> UTCTime -> IO ()
setSupportChatTs db groupMemberId chatTs =
DB.execute db "UPDATE group_members SET support_chat_ts = ? WHERE group_member_id = ?" (chatTs, groupMemberId)
createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> UTCTime -> IO ChatItemId
createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciContent quotedItem itemForwarded timed live createdAt =
createNewChatItem_ db user chatDirection createdByMsgId (Just sharedMsgId) ciContent quoteRow itemForwarded timed live False createdAt Nothing createdAt
@@ -750,7 +755,7 @@ findGroupChatPreviews_ db User {userId} pagination clq =
(
SELECT chat_item_id
FROM chat_items ci
WHERE ci.user_id = ? AND ci.group_id = g.group_id
WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL
ORDER BY ci.item_ts DESC
LIMIT 1
) AS chat_item_id,
@@ -1812,14 +1817,22 @@ getLocalNavInfo_ db User {userId} NoteFolder {noteFolderId} afterCI = do
:. (userId, noteFolderId, ciCreatedAt afterCI, cChatItemId afterCI)
)
toChatItemRef :: (ChatItemId, Maybe Int64, Maybe Int64, Maybe Int64) -> Either StoreError (ChatRef, ChatItemId)
toChatItemRef ::
(ChatItemId, Maybe ContactId, Maybe GroupId, Maybe GroupChatScopeTag, Maybe GroupMemberId, Maybe NoteFolderId) ->
Either StoreError (ChatRef, ChatItemId)
toChatItemRef = \case
(itemId, Just contactId, Nothing, Nothing) -> Right (ChatRef CTDirect contactId Nothing, itemId)
-- For "chat item refs" we don't care about scope, as they are only used for getting item by id (via getAChatItem).
-- To get the scope without joining group_members, we could additionally store group scope "tag" to differentiate member support chats.
(itemId, Nothing, Just groupId, Nothing) -> Right (ChatRef CTGroup groupId Nothing, itemId)
(itemId, Nothing, Nothing, Just folderId) -> Right (ChatRef CTLocal folderId Nothing, itemId)
(itemId, _, _, _) -> Left $ SEBadChatItem itemId Nothing
(itemId, Just contactId, Nothing, Nothing, Nothing, Nothing) ->
Right (ChatRef CTDirect contactId Nothing, itemId)
(itemId, Nothing, Just groupId, Nothing, Nothing, Nothing) ->
Right (ChatRef CTGroup groupId Nothing, itemId)
(itemId, Nothing, Just groupId, Just GCSTMemberSupport_, Nothing, Nothing) ->
Right (ChatRef CTGroup groupId (Just (GCSMemberSupport Nothing)), itemId)
(itemId, Nothing, Just groupId, Just GCSTMemberSupport_, Just scopeGMId, Nothing) ->
Right (ChatRef CTGroup groupId (Just (GCSMemberSupport $ Just scopeGMId)), itemId)
(itemId, Nothing, Nothing, Nothing, Nothing, Just folderId) ->
Right (ChatRef CTLocal folderId Nothing, itemId)
(itemId, _, _, _, _, _) ->
Left $ SEBadChatItem itemId Nothing
updateDirectChatItemsRead :: DB.Connection -> User -> ContactId -> IO ()
updateDirectChatItemsRead db User {userId} contactId = do
@@ -2170,7 +2183,7 @@ getAllChatItems db vr user@User {userId} pagination search_ = do
<$> DB.query
db
[sql|
SELECT chat_item_id, contact_id, group_id, note_folder_id
SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id
FROM chat_items
WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
ORDER BY item_ts DESC, chat_item_id DESC
@@ -2181,7 +2194,7 @@ getAllChatItems db vr user@User {userId} pagination search_ = do
DB.query
db
[sql|
SELECT chat_item_id, contact_id, group_id, note_folder_id
SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id
FROM chat_items
WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?))
@@ -2194,7 +2207,7 @@ getAllChatItems db vr user@User {userId} pagination search_ = do
<$> DB.query
db
[sql|
SELECT chat_item_id, contact_id, group_id, note_folder_id
SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id
FROM chat_items
WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?))
@@ -2206,7 +2219,7 @@ getAllChatItems db vr user@User {userId} pagination search_ = do
DB.query
db
[sql|
SELECT chat_item_id, contact_id, group_id, note_folder_id
SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id
FROM chat_items
WHERE chat_item_id = ?
|]
@@ -2952,7 +2965,7 @@ getChatItemByFileId db vr user@User {userId} fileId = do
DB.query
db
[sql|
SELECT i.chat_item_id, i.contact_id, i.group_id, i.note_folder_id
SELECT i.chat_item_id, i.contact_id, i.group_id, i.group_scope_tag, i.group_scope_group_member_id, i.note_folder_id
FROM chat_items i
JOIN files f ON f.chat_item_id = i.chat_item_id
WHERE f.user_id = ? AND f.file_id = ?
@@ -2974,7 +2987,7 @@ getChatItemByGroupId db vr user@User {userId} groupId = do
DB.query
db
[sql|
SELECT i.chat_item_id, i.contact_id, i.group_id, i.note_folder_id
SELECT i.chat_item_id, i.contact_id, i.group_id, i.group_scope_tag, i.group_scope_group_member_id, i.note_folder_id
FROM chat_items i
JOIN groups g ON g.chat_item_id = i.chat_item_id
WHERE g.user_id = ? AND g.group_id = ?
@@ -462,7 +462,7 @@ SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local
SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?)
Query:
SELECT chat_item_id, contact_id, group_id, note_folder_id
SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id
FROM chat_items
WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?))
@@ -474,7 +474,7 @@ SEARCH chat_items USING INDEX idx_chat_items_group_scope_item_ts (user_id=?)
USE TEMP B-TREE FOR ORDER BY
Query:
SELECT chat_item_id, contact_id, group_id, note_folder_id
SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id
FROM chat_items
WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
ORDER BY item_ts DESC, chat_item_id DESC
@@ -578,7 +578,7 @@ SEARCH p USING INTEGER PRIMARY KEY (rowid=?)
SEARCH i USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=? AND shared_msg_id=?) LEFT-JOIN
Query:
SELECT i.chat_item_id, i.contact_id, i.group_id, i.note_folder_id
SELECT i.chat_item_id, i.contact_id, i.group_id, i.group_scope_tag, i.group_scope_group_member_id, i.note_folder_id
FROM chat_items i
JOIN files f ON f.chat_item_id = i.chat_item_id
WHERE f.user_id = ? AND f.file_id = ?
@@ -1014,7 +1014,7 @@ Plan:
SEARCH chat_items USING INDEX idx_chat_items_group_id (group_id=?)
Query:
SELECT chat_item_id, contact_id, group_id, note_folder_id
SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id
FROM chat_items
WHERE chat_item_id = ?
@@ -1022,7 +1022,7 @@ Plan:
SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT chat_item_id, contact_id, group_id, note_folder_id
SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id
FROM chat_items
WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?))
@@ -1179,7 +1179,7 @@ SEARCH i USING INDEX idx_chat_items_group_id (group_id=?)
SEARCH m USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT i.chat_item_id, i.contact_id, i.group_id, i.note_folder_id
SELECT i.chat_item_id, i.contact_id, i.group_id, i.group_scope_tag, i.group_scope_group_member_id, i.note_folder_id
FROM chat_items i
JOIN groups g ON g.chat_item_id = i.chat_item_id
WHERE g.user_id = ? AND g.group_id = ?
@@ -1952,7 +1952,7 @@ Query:
(
SELECT chat_item_id
FROM chat_items ci
WHERE ci.user_id = ? AND ci.group_id = g.group_id
WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL
ORDER BY ci.item_ts DESC
LIMIT 1
) AS chat_item_id,
@@ -1995,7 +1995,7 @@ SEARCH gp USING INTEGER PRIMARY KEY (rowid=?)
SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
CORRELATED SCALAR SUBQUERY 1
SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
Query:
SELECT
@@ -2004,7 +2004,7 @@ Query:
(
SELECT chat_item_id
FROM chat_items ci
WHERE ci.user_id = ? AND ci.group_id = g.group_id
WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL
ORDER BY ci.item_ts DESC
LIMIT 1
) AS chat_item_id,
@@ -2041,7 +2041,7 @@ SEARCH g USING INDEX idx_groups_chat_ts (user_id=?)
SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
CORRELATED SCALAR SUBQUERY 1
SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
Query:
SELECT
@@ -2050,7 +2050,7 @@ Query:
(
SELECT chat_item_id
FROM chat_items ci
WHERE ci.user_id = ? AND ci.group_id = g.group_id
WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL
ORDER BY ci.item_ts DESC
LIMIT 1
) AS chat_item_id,
@@ -2086,7 +2086,7 @@ SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts<?)
SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
CORRELATED SCALAR SUBQUERY 1
SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
Query:
SELECT
@@ -2095,7 +2095,7 @@ Query:
(
SELECT chat_item_id
FROM chat_items ci
WHERE ci.user_id = ? AND ci.group_id = g.group_id
WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL
ORDER BY ci.item_ts DESC
LIMIT 1
) AS chat_item_id,
@@ -2131,7 +2131,7 @@ SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts>?)
SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
CORRELATED SCALAR SUBQUERY 1
SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
Query:
SELECT
@@ -2140,7 +2140,7 @@ Query:
(
SELECT chat_item_id
FROM chat_items ci
WHERE ci.user_id = ? AND ci.group_id = g.group_id
WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL
ORDER BY ci.item_ts DESC
LIMIT 1
) AS chat_item_id,
@@ -2176,7 +2176,7 @@ SEARCH g USING INDEX idx_groups_chat_ts (user_id=?)
SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
CORRELATED SCALAR SUBQUERY 1
SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
Query:
SELECT
@@ -2185,7 +2185,7 @@ Query:
(
SELECT chat_item_id
FROM chat_items ci
WHERE ci.user_id = ? AND ci.group_id = g.group_id
WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL
ORDER BY ci.item_ts DESC
LIMIT 1
) AS chat_item_id,
@@ -2221,7 +2221,7 @@ SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts<?)
SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
CORRELATED SCALAR SUBQUERY 1
SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
Query:
SELECT
@@ -2230,7 +2230,7 @@ Query:
(
SELECT chat_item_id
FROM chat_items ci
WHERE ci.user_id = ? AND ci.group_id = g.group_id
WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL
ORDER BY ci.item_ts DESC
LIMIT 1
) AS chat_item_id,
@@ -2266,7 +2266,7 @@ SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts>?)
SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
CORRELATED SCALAR SUBQUERY 1
SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
Query:
SELECT
@@ -2275,7 +2275,7 @@ Query:
(
SELECT chat_item_id
FROM chat_items ci
WHERE ci.user_id = ? AND ci.group_id = g.group_id
WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL
ORDER BY ci.item_ts DESC
LIMIT 1
) AS chat_item_id,
@@ -2311,7 +2311,7 @@ SEARCH g USING INDEX idx_groups_chat_ts (user_id=?)
SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
CORRELATED SCALAR SUBQUERY 1
SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
Query:
SELECT
@@ -2320,7 +2320,7 @@ Query:
(
SELECT chat_item_id
FROM chat_items ci
WHERE ci.user_id = ? AND ci.group_id = g.group_id
WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL
ORDER BY ci.item_ts DESC
LIMIT 1
) AS chat_item_id,
@@ -2353,7 +2353,7 @@ SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts<?)
SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
CORRELATED SCALAR SUBQUERY 1
SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
Query:
SELECT
@@ -2362,7 +2362,7 @@ Query:
(
SELECT chat_item_id
FROM chat_items ci
WHERE ci.user_id = ? AND ci.group_id = g.group_id
WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL
ORDER BY ci.item_ts DESC
LIMIT 1
) AS chat_item_id,
@@ -2395,7 +2395,7 @@ SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts>?)
SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
CORRELATED SCALAR SUBQUERY 1
SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
Query:
SELECT
@@ -2404,7 +2404,7 @@ Query:
(
SELECT chat_item_id
FROM chat_items ci
WHERE ci.user_id = ? AND ci.group_id = g.group_id
WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL
ORDER BY ci.item_ts DESC
LIMIT 1
) AS chat_item_id,
@@ -2437,7 +2437,7 @@ SEARCH g USING INDEX idx_groups_chat_ts (user_id=?)
SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN
CORRELATED SCALAR SUBQUERY 1
SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
Query:
SELECT
@@ -5460,7 +5460,7 @@ Plan:
Query: INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)
Plan:
Query: INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)
Query: INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)
Plan:
Query: INSERT INTO group_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)
+1 -1
View File
@@ -1858,7 +1858,7 @@ $(JQ.deriveJSON defaultJSON ''LocalProfile)
$(JQ.deriveJSON defaultJSON ''UserContactRequest)
$(JQ.deriveJSON (enumJSON $ dropPrefix "MC") ''MemberCriteria)
$(JQ.deriveJSON (enumJSON $ dropPrefix "MC") {J.tagSingleConstructors = True} ''MemberCriteria)
$(JQ.deriveJSON defaultJSON ''GroupMemberAdmission)
+5
View File
@@ -6955,6 +6955,11 @@ testScopedSupportManyModerators =
alice <# "#team (support: bob) dan> 5"
bob <# "#team (support) dan> 5"
alice @@@ [("#team","2"), ("@dan","sent invitation to join group team as moderator"), ("@cath","sent invitation to join group team as member"), ("@bob","sent invitation to join group team as member")]
bob @@@ [("#team","2"), ("@alice","received invitation to join group team as member")]
dan @@@ [("#team","2"), ("@alice","received invitation to join group team as moderator")]
cath @@@ [("#team","2"), ("@alice","received invitation to join group team as member")]
alice #$> ("/_get chat #1 count=3", chat, [(0, "connected"), (1, "1"), (0, "2")])
alice #$> ("/_get chat #1(_support:2) count=100", chat, [(1, "3"), (0, "4"), (0, "5")])
bob #$> ("/_get chat #1 count=3", chat, [(0, "connected"), (0, "1"), (1, "2")])