mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-26 19:35:48 +00:00
mobile: show group member images in the chat (#473)
* mobile: show group member images in the chat * improve layout for group chat * android: show member images in group chat * do not repeat member name in group messages
This commit is contained in:
committed by
GitHub
parent
8768d03e57
commit
5e964cf7e9
@@ -7,6 +7,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ArrowBack
|
||||
@@ -15,6 +16,7 @@ import androidx.compose.runtime.saveable.mapSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -25,6 +27,7 @@ import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.chat.item.ChatItemView
|
||||
import chat.simplex.app.views.chatlist.openChat
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.ModalManager
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
@@ -62,6 +65,12 @@ fun ChatView(chatModel: ChatModel) {
|
||||
ChatLayout(user, chat, chatModel.chatItems, msg, quotedItem, editingItem,
|
||||
back = { chatModel.chatId.value = null },
|
||||
info = { ModalManager.shared.showCustomModal { close -> ChatInfoView(chatModel, close) } },
|
||||
openDirectChat = { contactId ->
|
||||
val c = chatModel.chats.firstOrNull {
|
||||
it.chatInfo is ChatInfo.Direct && it.chatInfo.contact.contactId == contactId
|
||||
}
|
||||
if (c != null) withApi { openChat(chatModel, c.chatInfo) }
|
||||
},
|
||||
sendMessage = { msg ->
|
||||
withApi {
|
||||
// show "in progress"
|
||||
@@ -104,6 +113,7 @@ fun ChatLayout(
|
||||
editingItem: MutableState<ChatItem?>,
|
||||
back: () -> Unit,
|
||||
info: () -> Unit,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
sendMessage: (String) -> Unit,
|
||||
resetMessage: () -> Unit
|
||||
) {
|
||||
@@ -119,7 +129,7 @@ fun ChatLayout(
|
||||
modifier = Modifier.navigationBarsWithImePadding()
|
||||
) { contentPadding ->
|
||||
Box(Modifier.padding(contentPadding)) {
|
||||
ChatItemsList(user, chatItems, msg, quotedItem, editingItem)
|
||||
ChatItemsList(user, chat, chatItems, msg, quotedItem, editingItem, openDirectChat)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,10 +196,12 @@ val CIListStateSaver = run {
|
||||
@Composable
|
||||
fun ChatItemsList(
|
||||
user: User,
|
||||
chat: Chat,
|
||||
chatItems: List<ChatItem>,
|
||||
msg: MutableState<String>,
|
||||
quotedItem: MutableState<ChatItem?>,
|
||||
editingItem: MutableState<ChatItem?>
|
||||
editingItem: MutableState<ChatItem?>,
|
||||
openDirectChat: (Long) -> Unit
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val keyboardState by getKeyboardState()
|
||||
@@ -200,8 +212,42 @@ fun ChatItemsList(
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val cxt = LocalContext.current
|
||||
LazyColumn(state = listState) {
|
||||
items(chatItems) { cItem ->
|
||||
ChatItemView(user, cItem, msg, quotedItem, editingItem, cxt, uriHandler)
|
||||
itemsIndexed(chatItems) { i, cItem ->
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
if (cItem.chatDir is CIDirection.GroupRcv) {
|
||||
val prevItem = if (i > 0) chatItems[i - 1] else null
|
||||
val member = cItem.chatDir.groupMember
|
||||
val showMember = showMemberImage(member, prevItem)
|
||||
Row(Modifier.padding(start = 8.dp, end = 66.dp)) {
|
||||
if (showMember) {
|
||||
val contactId = member.memberContactId
|
||||
if (contactId == null) {
|
||||
MemberImage(member)
|
||||
} else {
|
||||
Box(Modifier.clip(CircleShape).clickable { openDirectChat(contactId) }) {
|
||||
MemberImage(member)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.size(4.dp))
|
||||
} else {
|
||||
Spacer(Modifier.size(42.dp))
|
||||
}
|
||||
ChatItemView(user, cItem, msg, quotedItem, editingItem, cxt, uriHandler, showMember = showMember)
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.padding(start = 86.dp, end = 12.dp)) {
|
||||
ChatItemView(user, cItem, msg, quotedItem, editingItem, cxt, uriHandler)
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
val sent = cItem.chatDir.sent
|
||||
Box(Modifier.padding(
|
||||
start = if (sent) 76.dp else 12.dp,
|
||||
end = if (sent) 12.dp else 76.dp,
|
||||
)) {
|
||||
ChatItemView(user, cItem, msg, quotedItem, editingItem, cxt, uriHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
val len = chatItems.count()
|
||||
if (len > 1 && (keyboardState != ciListState.value.keyboardState || !ciListState.value.scrolled || len != ciListState.value.itemCount)) {
|
||||
@@ -213,6 +259,18 @@ fun ChatItemsList(
|
||||
}
|
||||
}
|
||||
|
||||
fun showMemberImage(member: GroupMember, prevItem: ChatItem?): Boolean {
|
||||
return prevItem == null || prevItem.chatDir is CIDirection.GroupSnd ||
|
||||
( prevItem.chatDir is CIDirection.GroupRcv &&
|
||||
prevItem.chatDir.groupMember.groupMemberId != member.groupMemberId
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MemberImage(member: GroupMember) {
|
||||
ProfileImage(38.dp, member.memberProfile.image)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
@@ -252,6 +310,48 @@ fun PreviewChatLayout() {
|
||||
editingItem = remember { mutableStateOf(null) },
|
||||
back = {},
|
||||
info = {},
|
||||
openDirectChat = {},
|
||||
sendMessage = {},
|
||||
resetMessage = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun PreviewGroupChatLayout() {
|
||||
SimpleXTheme {
|
||||
val chatItems = listOf(
|
||||
ChatItem.getSampleData(
|
||||
1, CIDirection.GroupSnd(), Clock.System.now(), "hello"
|
||||
),
|
||||
ChatItem.getSampleData(
|
||||
2, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello"
|
||||
),
|
||||
ChatItem.getSampleData(
|
||||
3, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello"
|
||||
),
|
||||
ChatItem.getSampleData(
|
||||
4, CIDirection.GroupSnd(), Clock.System.now(), "hello"
|
||||
),
|
||||
ChatItem.getSampleData(
|
||||
5, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello"
|
||||
)
|
||||
)
|
||||
ChatLayout(
|
||||
user = User.sampleData,
|
||||
chat = Chat(
|
||||
chatInfo = ChatInfo.Group.sampleData,
|
||||
chatItems = chatItems,
|
||||
chatStats = Chat.ChatStats()
|
||||
),
|
||||
chatItems = chatItems,
|
||||
msg = remember { mutableStateOf("") },
|
||||
quotedItem = remember { mutableStateOf(null) },
|
||||
editingItem = remember { mutableStateOf(null) },
|
||||
back = {},
|
||||
info = {},
|
||||
openDirectChat = {},
|
||||
sendMessage = {},
|
||||
resetMessage = {}
|
||||
)
|
||||
|
||||
@@ -30,7 +30,8 @@ fun ChatItemView(
|
||||
quotedItem: MutableState<ChatItem?>,
|
||||
editingItem: MutableState<ChatItem?>,
|
||||
cxt: Context,
|
||||
uriHandler: UriHandler? = null
|
||||
uriHandler: UriHandler? = null,
|
||||
showMember: Boolean = false,
|
||||
) {
|
||||
val sent = cItem.chatDir.sent
|
||||
val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart
|
||||
@@ -38,18 +39,14 @@ fun ChatItemView(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 4.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = if (sent) 86.dp else 16.dp,
|
||||
end = if (sent) 16.dp else 86.dp,
|
||||
),
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = alignment,
|
||||
) {
|
||||
Column(Modifier.combinedClickable(onLongClick = { showMenu = true }, onClick = {})) {
|
||||
if (cItem.quotedItem == null && isShortEmoji(cItem.content.text)) {
|
||||
EmojiItemView(cItem)
|
||||
} else {
|
||||
FramedItemView(user, cItem, uriHandler)
|
||||
FramedItemView(user, cItem, uriHandler, showMember = showMember)
|
||||
}
|
||||
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
|
||||
ItemAction("Reply", Icons.Outlined.Reply, onClick = {
|
||||
|
||||
@@ -23,7 +23,7 @@ val SentQuoteColorLight = Color(0x2545B8FF)
|
||||
val ReceivedQuoteColorLight = Color(0x25B1B0B5)
|
||||
|
||||
@Composable
|
||||
fun FramedItemView(user: User, ci: ChatItem, uriHandler: UriHandler? = null) {
|
||||
fun FramedItemView(user: User, ci: ChatItem, uriHandler: UriHandler? = null, showMember: Boolean = false) {
|
||||
val sent = ci.chatDir.sent
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
@@ -58,7 +58,7 @@ fun FramedItemView(user: User, ci: ChatItem, uriHandler: UriHandler? = null) {
|
||||
}
|
||||
} else {
|
||||
MarkdownText(
|
||||
ci.content, ci.formattedText, ci.memberDisplayName,
|
||||
ci.content, ci.formattedText, if (showMember) ci.memberDisplayName else null,
|
||||
metaText = ci.timestampText, edited = ci.meta.itemEdited, uriHandler = uriHandler, senderBold = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -164,6 +164,14 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func getPrevChatItem(_ ci: ChatItem) -> ChatItem? {
|
||||
if let i = chatItems.firstIndex(where: { $0.id == ci.id }), i > 0 {
|
||||
return chatItems[i - 1]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func popChat(_ id: String) {
|
||||
if let i = getChatIndex(id) {
|
||||
popChat_(i)
|
||||
@@ -519,6 +527,16 @@ struct GroupMember: Decodable {
|
||||
var memberContactId: Int64?
|
||||
// var activeConn: Connection?
|
||||
|
||||
var directChatId: ChatId? {
|
||||
get {
|
||||
if let chatId = memberContactId {
|
||||
return "@\(chatId)"
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static let sampleData = GroupMember(
|
||||
groupMemberId: 1,
|
||||
memberId: "abcd",
|
||||
|
||||
@@ -16,6 +16,7 @@ private let sentQuoteColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, o
|
||||
struct FramedItemView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var chatItem: ChatItem
|
||||
var showMember = false
|
||||
@State var msgWidth: CGFloat = 0
|
||||
|
||||
var body: some View {
|
||||
@@ -53,7 +54,7 @@ struct FramedItemView: View {
|
||||
MsgContentView(
|
||||
content: chatItem.content,
|
||||
formattedText: chatItem.formattedText,
|
||||
sender: chatItem.memberDisplayName,
|
||||
sender: showMember ? chatItem.memberDisplayName : nil,
|
||||
metaText: chatItem.timestampText,
|
||||
edited: chatItem.meta.itemEdited
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ struct MsgContentView: View {
|
||||
var formattedText: [FormattedText]? = nil
|
||||
var sender: String? = nil
|
||||
var metaText: Text? = nil
|
||||
var edited: Bool = false
|
||||
var edited = false
|
||||
|
||||
var body: some View {
|
||||
let v = messageText(content, formattedText, sender)
|
||||
|
||||
@@ -10,12 +10,13 @@ import SwiftUI
|
||||
|
||||
struct ChatItemView: View {
|
||||
var chatItem: ChatItem
|
||||
var showMember = false
|
||||
|
||||
var body: some View {
|
||||
if (chatItem.quotedItem == nil && isShortEmoji(chatItem.content.text)) {
|
||||
EmojiItemView(chatItem: chatItem)
|
||||
} else {
|
||||
FramedItemView(chatItem: chatItem)
|
||||
FramedItemView(chatItem: chatItem, showMember: showMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
private let memberImageSize: CGFloat = 34
|
||||
|
||||
struct ChatView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@@ -24,39 +26,32 @@ struct ChatView: View {
|
||||
|
||||
return VStack {
|
||||
GeometryReader { g in
|
||||
let maxWidth = g.size.width * 0.78
|
||||
let maxWidth =
|
||||
cInfo.chatType == .group
|
||||
? (g.size.width - 28) * 0.84 - 42
|
||||
: (g.size.width - 32) * 0.84
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 5) {
|
||||
ForEach(chatModel.chatItems) { ci in
|
||||
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
|
||||
ChatItemView(chatItem: ci)
|
||||
.contextMenu {
|
||||
Button {
|
||||
withAnimation {
|
||||
editingItem = nil
|
||||
quotedItem = ci
|
||||
}
|
||||
} label: { Label("Reply", systemImage: "arrowshape.turn.up.left") }
|
||||
Button {
|
||||
showShareSheet(items: [ci.content.text])
|
||||
} label: { Label("Share", systemImage: "square.and.arrow.up") }
|
||||
Button {
|
||||
UIPasteboard.general.string = ci.content.text
|
||||
} label: { Label("Copy", systemImage: "doc.on.doc") }
|
||||
// if (ci.chatDir.sent && ci.meta.editable) {
|
||||
// Button {
|
||||
// withAnimation {
|
||||
// quotedItem = nil
|
||||
// editingItem = ci
|
||||
// message = ci.content.text
|
||||
// }
|
||||
// } label: { Label("Edit", systemImage: "square.and.pencil") }
|
||||
// }
|
||||
if case let .groupRcv(member) = ci.chatDir {
|
||||
let prevItem = chatModel.getPrevChatItem(ci)
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
let showMember = showMemberImage(member, prevItem)
|
||||
if showMember {
|
||||
ProfileImage(imageStr: member.memberProfile.image)
|
||||
.frame(width: memberImageSize, height: memberImageSize)
|
||||
} else {
|
||||
Rectangle().fill(.clear)
|
||||
.frame(width: memberImageSize, height: memberImageSize)
|
||||
}
|
||||
chatItemWithMenu(ci, maxWidth, showMember: showMember).padding(.leading, 8)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: alignment)
|
||||
.padding(.trailing)
|
||||
.padding(.leading, 12)
|
||||
} else {
|
||||
chatItemWithMenu(ci, maxWidth).padding(.horizontal)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
DispatchQueue.main.async {
|
||||
@@ -119,6 +114,44 @@ struct ChatView: View {
|
||||
.navigationBarBackButtonHidden(true)
|
||||
}
|
||||
|
||||
private func chatItemWithMenu(_ ci: ChatItem, _ maxWidth: CGFloat, showMember: Bool = false) -> some View {
|
||||
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
|
||||
return ChatItemView(chatItem: ci, showMember: showMember)
|
||||
.contextMenu {
|
||||
Button {
|
||||
withAnimation {
|
||||
editingItem = nil
|
||||
quotedItem = ci
|
||||
}
|
||||
} label: { Label("Reply", systemImage: "arrowshape.turn.up.left") }
|
||||
Button {
|
||||
showShareSheet(items: [ci.content.text])
|
||||
} label: { Label("Share", systemImage: "square.and.arrow.up") }
|
||||
Button {
|
||||
UIPasteboard.general.string = ci.content.text
|
||||
} label: { Label("Copy", systemImage: "doc.on.doc") }
|
||||
// if (ci.chatDir.sent && ci.meta.editable) {
|
||||
// Button {
|
||||
// withAnimation {
|
||||
// quotedItem = nil
|
||||
// editingItem = ci
|
||||
// message = ci.content.text
|
||||
// }
|
||||
// } label: { Label("Edit", systemImage: "square.and.pencil") }
|
||||
// }
|
||||
}
|
||||
.frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: alignment)
|
||||
}
|
||||
|
||||
private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool {
|
||||
switch (prevItem?.chatDir) {
|
||||
case .groupSnd: return true
|
||||
case let .groupRcv(prevMember): return prevMember.groupMemberId != member.groupMemberId
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
func scrollToBottom(_ proxy: ScrollViewProxy, animation: Animation = .default) {
|
||||
withAnimation(animation) { scrollToBottom_(proxy) }
|
||||
}
|
||||
|
||||
@@ -253,8 +253,8 @@ msgPreview :: MsgContent -> [StyledString]
|
||||
msgPreview = msgPlain . preview . msgContentText
|
||||
where
|
||||
preview t
|
||||
| T.length t <= 60 = t
|
||||
| otherwise = t <> "..."
|
||||
| T.length t <= 120 = t
|
||||
| otherwise = T.take 120 t <> "..."
|
||||
|
||||
viewMsgIntegrityError :: MsgErrorType -> [StyledString]
|
||||
viewMsgIntegrityError err = msgError $ case err of
|
||||
|
||||
Reference in New Issue
Block a user