From e979b7efdcfe74e30d4025102366ab29b6f25d81 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:12:39 +0000 Subject: [PATCH] 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 Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/ios/Shared/Views/Chat/ChatView.swift | 58 +++++----- .../simplex/common/views/chat/ChatView.kt | 4 +- ...channel-received-no-avatar-left-padding.md | 103 ++++++++++++++++++ 3 files changed, 137 insertions(+), 28 deletions(-) create mode 100644 plans/2026-06-20-channel-received-no-avatar-left-padding.md diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index ded0019692..283157864d 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -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) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 765759d9e0..4a3bfe4208 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -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) ) { diff --git a/plans/2026-06-20-channel-received-no-avatar-left-padding.md b/plans/2026-06-20-channel-received-no-avatar-left-padding.md new file mode 100644 index 0000000000..13ee4ebc4f --- /dev/null +++ b/plans/2026-06-20-channel-received-no-avatar-left-padding.md @@ -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.