android, desktop, ios: remove right gap on received messages in channels (#7106)

* android, desktop, ios: remove right gap on received messages in channels

In channels received messages now use the full row width instead of the chat-bubble right gap, matching the broadcast/feed style. Gated on ChatInfo.isChannel (useRelays), the always-present channel flag used across the channel UI; sent messages and non-channel groups, business and direct chats are unchanged.

* docs: add plan justifying removing right gap on received messages in channels

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Narasimha-sc
2026-06-20 13:50:46 +00:00
committed by GitHub
parent 2df131bd05
commit 134e48fe7e
3 changed files with 101 additions and 5 deletions
+2 -1
View File
@@ -895,8 +895,9 @@ struct ChatView: View {
}
} else {
let voiceNoFrame = voiceWithoutFrame(ci)
let channelReceived = !ci.chatDir.sent && cInfo.isChannel
let maxWidth = cInfo.chatType == .group
? voiceNoFrame
? voiceNoFrame || channelReceived
? (g.size.width - 28) - 42
: (g.size.width - 28) * 0.84 - 42
: voiceNoFrame
@@ -2000,7 +2000,7 @@ fun BoxScope.ChatItemsList(
Column(
Modifier
.padding(top = 8.dp)
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
.padding(start = 8.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
.fillMaxWidth()
.then(swipeableModifier),
verticalArrangement = Arrangement.spacedBy(4.dp),
@@ -2079,7 +2079,7 @@ fun BoxScope.ChatItemsList(
}
Row(
Modifier
.padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
.padding(start = 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)
) {
@@ -2092,7 +2092,7 @@ fun BoxScope.ChatItemsList(
Column(
Modifier
.padding(top = 8.dp)
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
.padding(start = 8.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
.fillMaxWidth()
.then(swipeableModifier),
verticalArrangement = Arrangement.spacedBy(4.dp),
@@ -2162,7 +2162,7 @@ fun BoxScope.ChatItemsList(
}
Row(
Modifier
.padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
.padding(start = 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,95 @@
# Remove the right gap on received messages in channels
## Problem
In groups, received messages are laid out as left-aligned chat bubbles whose
maximum width is capped well short of the right edge, leaving a large empty gap
on the right so long content wraps early. In channels this wastes horizontal
space — channel posts are broadcast/feed-style content that reads better using
nearly the full row width.
## Change
For channels only, received messages drop the right-side gap so content can use
nearly the full row width (a small edge margin remains). This only changes the
maximum available width: long text uses more of the row, short messages still
size to content, and media stays within its existing cap. Sent messages keep
their existing layout.
### Android / desktop (`apps/multiplatform`)
The `end` padding becomes `12.dp` (the same edge margin sent messages use)
instead of `adjustTailPaddingOffset(66.dp, …)`, at the four received-message
layout sites in `ChatItemsList` (`ChatView.kt`): the `GroupRcv`
(member-attributed) and `ChannelRcv` (unattributed) branches, each with and
without an avatar.
```kotlin
end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp
else adjustTailPaddingOffset(66.dp, start = false)
```
### iOS (`apps/ios`)
iOS computes one per-message `maxWidth` in `ChatView.swift` and applies it to
every bubble; the `* 0.84` factor is the gap. For a received message in a
channel that factor is dropped (full width minus the avatar inset) — the same
geometry the voice-message case already uses:
```swift
let channelReceived = !ci.chatDir.sent && cInfo.isChannel
let maxWidth = cInfo.chatType == .group
? voiceNoFrame || channelReceived
? (g.size.width - 28) - 42
: (g.size.width - 28) * 0.84 - 42
: ...
```
The received check (`!ci.chatDir.sent`) is explicit here because, unlike the
Kotlin layout (which has a separate received branch), iOS shares one `maxWidth`
between sent and received.
## Why gate on `ChatInfo.isChannel` (`useRelays`)
The change is gated per chat on `ChatInfo.isChannel`, which is
`groupInfo?.useRelays == true``chatInfo.isChannel` on both Android/desktop and
iOS (`cInfo.isChannel`).
This is the robust signal. The whole channel feature on both platforms keys on
`useRelays` (channel preferences, member management, info view, broadcast
compose, etc.); `useRelays` is a non-optional `Bool` that is always present on a
group.
- **Not on the group-type `isChannel`** (`publicGroup?.groupType == channel`).
This was the first attempt and it left the gap in place on iOS. The likely
mechanism: `publicGroup` is an optional reconstructed from nullable DB columns
(`src/Simplex/Chat/Store/Groups.hs` `toGroupProfile`, plus a creation path that
sets `publicGroup = Nothing`), so when it is not populated for a chat the
optional chain silently evaluates to `false` and the gap is never removed.
`useRelays` cannot fail this way — it is a required `Bool` set at group
creation (`useRelays = not direct`, `Commands.hs:2080`). Independent of the
exact mechanism, `useRelays` is the safer signal. It is also as precise: the
only group type ever constructed is `GTChannel` (`GTGroup` is defined but never
instantiated), and `useRelays == true` is set on exactly that same
public-group/channel path, so `useRelays == true` ⟺ "is a channel" for every
chat today — regular groups, business chats and direct chats all have
`useRelays` false/absent (verified: no non-channel path sets it true).
- **Not on the item direction.** The unattributed `ChannelRcv` direction is
produced for any group message without an attributed member, not only in
channels, and channels also contain member-attributed (`GroupRcv`) posts.
Gating on direction would both over- and under-match, so the gate is the
per-chat `isChannel`.
## Scope
Regular groups, business chats, and direct chats are unchanged (`isChannel` is
false for them). Sent messages are untouched.
## Verification
- Android/desktop: `:common:compileKotlinDesktop` compiles clean.
- iOS: change is a small, type-safe Swift expression; build/verify on macOS
(Xcode) — not compilable on the Linux build host used here.
- Visual (both platforms): in a channel, long received messages widen toward the
right edge; in a regular group and in direct chats the right gap is unchanged;
sent messages are unchanged everywhere.