mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 00:02:28 +00:00
android, desktop: group knocking (#5816)
This commit is contained in:
1 parent
3095b980cb
commit
805a69da94
34 files changed
+1844
-748
No files matched your search
+3
@@ -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)
|
||||
}
|
||||
|
||||
+235
-107
@@ -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) {
|
||||
|
||||
+109
-71
@@ -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")
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ expect fun PlatformTextField(
|
||||
textStyle: MutableState<TextStyle>,
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
userIsPending: Boolean,
|
||||
placeholder: String,
|
||||
showVoiceButton: Boolean,
|
||||
onMessageChange: (ComposeMessage) -> Unit,
|
||||
|
||||
+1
@@ -104,6 +104,7 @@ fun TerminalLayout(
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = false,
|
||||
userIsObserver = false,
|
||||
userIsPending = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
placeholder = "",
|
||||
|
||||
+6
-4
@@ -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) {
|
||||
|
||||
+3
-3
@@ -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)
|
||||
|
||||
+472
-426
File diff suppressed because it is too large.
Load diff
+116
@@ -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()
|
||||
}
|
||||
}
|
||||
+31
-7
@@ -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,
|
||||
|
||||
+1
-1
@@ -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()
|
||||
|
||||
+5
-2
@@ -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,
|
||||
|
||||
+1
-1
@@ -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
|
||||
)
|
||||
|
||||
+102
-30
@@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
+30
@@ -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)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+20
@@ -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 }
|
||||
|
||||
|
||||
+26
@@ -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,
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
+152
@@ -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,
|
||||
)
|
||||
}
|
||||
+122
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+260
@@ -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
|
||||
// })
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -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(),
|
||||
|
||||
+5
-4
@@ -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>
|
||||
|
||||
+3
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (?,?,?,?,?,?,?)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")])
|
||||
|
||||
Reference in new issue
Block a user