mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-29 07:38:54 +00:00
android, desktop, ios: remove left padding on consecutive received messages in channels (#7108)
* android, desktop, ios: remove left padding on consecutive received messages in channels In channels, a received message that does not show an avatar (a consecutive post from the same sender) drops the avatar-sized left padding and sits flush-left. Applies to both owner broadcasts (ChannelRcv) and contributor posts (GroupRcv); the first message of each run still shows the avatar. Gated on ChatInfo.isChannel, so regular groups, business and direct chats, sent messages, and avatar-shown messages are unchanged. * docs: add plan justifying removing left padding on consecutive received messages in channels * ios: fix right gap on consecutive received messages in channels Removing the avatar-sized left padding from no-avatar received messages (this PR) shifted those bubbles ~44pt left, but maxWidth still reserved the avatar inset, so consecutive messages stopped ~44pt short of the first (avatar) message on the right. Widen maxWidth for no-avatar channel-received items so their right edge matches the avatar-shown first message. The no-avatar predicate reuses the exact shouldShowAvatar expression from the render path (lifted to a file-scope function so the maxWidth site can call it), so the width and the rendered layout can never disagree. Android is unaffected: Compose derives content width from padding, so reducing the start padding already widened the row there. * ios: increase left padding * kotlin: increase left gap --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
co-authored by
Evgeny Poberezkin
Evgeny Poberezkin
parent
3a38c39d35
commit
e979b7efdc
@@ -14,6 +14,29 @@ import Combine
|
||||
|
||||
private let memberImageSize: CGFloat = 34
|
||||
|
||||
private func shouldShowAvatar(_ current: ChatItem, _ older: ChatItem?) -> Bool {
|
||||
let oldIsGroupRcv = switch older?.chatDir {
|
||||
case .groupRcv: true
|
||||
case .channelRcv: true
|
||||
default: false
|
||||
}
|
||||
let sameMember = switch (older?.chatDir, current.chatDir) {
|
||||
case (.groupRcv(let oldMember), .groupRcv(let member)):
|
||||
oldMember.memberId == member.memberId
|
||||
case (.channelRcv, .channelRcv):
|
||||
true
|
||||
default:
|
||||
false
|
||||
}
|
||||
if case .groupRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) {
|
||||
return true
|
||||
} else if case .channelRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Spec: spec/client/chat-view.md#ChatView
|
||||
struct ChatView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@@ -896,8 +919,14 @@ struct ChatView: View {
|
||||
} else {
|
||||
let voiceNoFrame = voiceWithoutFrame(ci)
|
||||
let channelReceived = !ci.chatDir.sent && cInfo.isChannel
|
||||
// consecutive (no-avatar) received messages in channels drop the avatar-sized
|
||||
// left padding (see .leading padding below), so they get the full row width here
|
||||
// too — otherwise the reserved avatar inset would leave a gap on the right
|
||||
let channelReceivedNoAvatar = channelReceived && !shouldShowAvatar(mergedItem.newest().item, mergedItem.oldest().nextItem)
|
||||
let maxWidth = cInfo.chatType == .group
|
||||
? voiceNoFrame || channelReceived
|
||||
? channelReceivedNoAvatar
|
||||
? g.size.width - 26
|
||||
: voiceNoFrame || channelReceived
|
||||
? (g.size.width - 28) - 42
|
||||
: (g.size.width - 28) * 0.84 - 42
|
||||
: voiceNoFrame
|
||||
@@ -1733,29 +1762,6 @@ struct ChatView: View {
|
||||
)
|
||||
}
|
||||
|
||||
func shouldShowAvatar(_ current: ChatItem, _ older: ChatItem?) -> Bool {
|
||||
let oldIsGroupRcv = switch older?.chatDir {
|
||||
case .groupRcv: true
|
||||
case .channelRcv: true
|
||||
default: false
|
||||
}
|
||||
let sameMember = switch (older?.chatDir, current.chatDir) {
|
||||
case (.groupRcv(let oldMember), .groupRcv(let member)):
|
||||
oldMember.memberId == member.memberId
|
||||
case (.channelRcv, .channelRcv):
|
||||
true
|
||||
default:
|
||||
false
|
||||
}
|
||||
if case .groupRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) {
|
||||
return true
|
||||
} else if case .channelRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let last = isLastItem ? im.reversedChatItems.last : nil
|
||||
let listItem = merged.newest()
|
||||
@@ -1979,7 +1985,7 @@ struct ChatView: View {
|
||||
}
|
||||
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
|
||||
.padding(.trailing)
|
||||
.padding(.leading, 10 + memberImageSize + 12)
|
||||
.padding(.leading, chat.chatInfo.isChannel ? nil : 10 + memberImageSize + 12)
|
||||
}
|
||||
.padding(.bottom, bottomPadding)
|
||||
}
|
||||
@@ -2076,7 +2082,7 @@ struct ChatView: View {
|
||||
}
|
||||
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
|
||||
.padding(.trailing)
|
||||
.padding(.leading, 10 + memberImageSize + 12)
|
||||
.padding(.leading, chat.chatInfo.isChannel ? nil : 10 + memberImageSize + 12)
|
||||
}
|
||||
.padding(.bottom, bottomPadding)
|
||||
}
|
||||
|
||||
+2
-2
@@ -2084,7 +2084,7 @@ fun BoxScope.ChatItemsList(
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
||||
.padding(start = if (chatInfo.isChannel) 12.dp else 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
||||
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
|
||||
.then(swipeableOrSelectionModifier)
|
||||
) {
|
||||
@@ -2167,7 +2167,7 @@ fun BoxScope.ChatItemsList(
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
||||
.padding(start = if (chatInfo.isChannel) 12.dp else 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
||||
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
|
||||
.then(swipeableOrSelectionModifier)
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# Remove left padding on consecutive (no-avatar) received messages in channels
|
||||
|
||||
## Problem
|
||||
|
||||
In a channel, received messages show the sender avatar on the first message of a
|
||||
run and hide it on consecutive messages, but those consecutive messages still
|
||||
reserve the avatar-sized **left padding** so they line up under the first. For a
|
||||
channel's feed-style layout this indentation wastes horizontal space —
|
||||
consecutive received messages should sit flush-left where the avatar would be.
|
||||
This applies to **both** the channel owner's broadcasts and contributors' posts.
|
||||
|
||||
Desired behaviour: in channels, any received message that does **not** show an
|
||||
avatar (a consecutive post from the same sender) drops the avatar-sized left
|
||||
padding. The first message of a run still shows the avatar and keeps its layout;
|
||||
when the run is broken (a different sender, or a time gap), the next message
|
||||
shows the avatar again — this run logic is unchanged, only the no-avatar left
|
||||
padding is reduced.
|
||||
|
||||
## The two received directions in a channel
|
||||
|
||||
Received items in a channel arrive as one of two directions
|
||||
(`Subscriber.hs`, `saveRcvCI`):
|
||||
|
||||
- **`ChannelRcv`** (no member) — the **owner's** broadcast, sent "as the channel".
|
||||
The backend permits sending-as-group only to the owner, and a channel owner's
|
||||
main-scope messages are always sent as group (`ChatInfo.sendAsGroup` is true
|
||||
for `useRelays && memberRole >= Owner` in the main scope), so received owner
|
||||
posts arrive as `ChannelRcv`. Shows the channel avatar.
|
||||
- **`GroupRcv(member)`** (attributed) — a **contributor's** post, carrying the
|
||||
member. Shows the member avatar.
|
||||
|
||||
Both are received messages, and the change now applies to **both** when they hide
|
||||
the avatar. (An earlier revision scoped this to `ChannelRcv`/owner only; it now
|
||||
covers contributors too, per request.)
|
||||
|
||||
## Change
|
||||
|
||||
In channels — gated on `ChatInfo.isChannel` (the `useRelays` flag, which is
|
||||
reliably present, unlike the optional group-type predicate) — the no-avatar
|
||||
branches for **both** `ChannelRcv` and `GroupRcv` drop the avatar-sized left
|
||||
padding down to the base inset where the avatar itself starts. In non-channel
|
||||
groups the `GroupRcv` no-avatar layout is unchanged (`isChannel` is false). The
|
||||
avatar-shown layouts, sent messages, and all other chats are unchanged.
|
||||
|
||||
The same Row's `end` padding is already gated on `chatInfo.isChannel` (the merged
|
||||
right-gap change #7106), so gating `start` on `isChannel` keeps each Row
|
||||
internally consistent and the change precisely "in channels".
|
||||
|
||||
### Android / desktop (`apps/multiplatform`, `ChatView.kt`, `ChatItemsList`)
|
||||
|
||||
Both the `CIDirection.GroupRcv` and `CIDirection.ChannelRcv` `showAvatar == false`
|
||||
rows:
|
||||
|
||||
```kotlin
|
||||
// before
|
||||
.padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = …)
|
||||
// after
|
||||
.padding(start = if (chatInfo.isChannel) 8.dp else 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = …)
|
||||
```
|
||||
|
||||
### iOS (`apps/ios`, `ChatView.swift`, `chatItemListView`)
|
||||
|
||||
Both the `.groupRcv` and `.channelRcv` no-avatar branches:
|
||||
|
||||
```swift
|
||||
// before
|
||||
.padding(.leading, 10 + memberImageSize + 12)
|
||||
// after
|
||||
.padding(.leading, chat.chatInfo.isChannel ? 12 : 10 + memberImageSize + 12)
|
||||
```
|
||||
|
||||
## Run behaviour (unchanged)
|
||||
|
||||
`shouldShowAvatar(current, older)` shows the avatar on the first message of a
|
||||
same-sender run and hides it on consecutive ones; a different sender or a gap
|
||||
resets the run. For `GroupRcv` "same sender" is the same `memberId`; for
|
||||
`ChannelRcv` consecutive channel broadcasts count as the same sender. Only the
|
||||
no-avatar left padding is changed.
|
||||
|
||||
## Scope
|
||||
|
||||
- Affects: all received consecutive (no-avatar) messages **in channels** — owner
|
||||
broadcasts (`ChannelRcv`) and contributor posts (`GroupRcv`). This includes a
|
||||
channel's member-support sub-scope, which renders through the same
|
||||
`ChatItemsList` with the channel's `isChannel`; treating it the same way is
|
||||
consistent with the merged right-gap change (#7106), which also gates that
|
||||
Row's `end` padding on `isChannel` without a scope filter.
|
||||
- Unchanged: the first message of each run (avatar shown), sent messages, regular
|
||||
groups, business chats and direct chats (`isChannel` false — the `else` branch
|
||||
preserves the original avatar-inset value exactly), and any non-channel
|
||||
`ChannelRcv` welcome item.
|
||||
|
||||
## Verification
|
||||
|
||||
- Android/desktop: `:common:compileKotlinDesktop` compiles clean.
|
||||
- iOS: small, type-safe constant change; build/verify on macOS (Xcode) — not
|
||||
compilable on the Linux build host used here.
|
||||
- Visual (both platforms), in a channel:
|
||||
- First message of a run (owner or contributor): avatar shown, layout unchanged.
|
||||
- Following messages from the same sender (no avatar): now flush-left.
|
||||
- A different sender / time gap resets the run — the next message shows the
|
||||
avatar again.
|
||||
- Regular groups, business and direct chats keep their existing indentation.
|
||||
Reference in New Issue
Block a user