Merge branch 'master' into master-android

This commit is contained in:
Evgeny Poberezkin
2026-07-22 16:51:58 +01:00
13 changed files with 194 additions and 71 deletions
+18 -18
View File
@@ -188,8 +188,8 @@
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; };
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.9-J30uILApgSuBRyrol6CHua-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.9-J30uILApgSuBRyrol6CHua-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.9-J30uILApgSuBRyrol6CHua.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.9-J30uILApgSuBRyrol6CHua.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.10-Fr7Tzkw2TAVKpbtViCLw34-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.10-Fr7Tzkw2TAVKpbtViCLw34-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.10-Fr7Tzkw2TAVKpbtViCLw34.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.10-Fr7Tzkw2TAVKpbtViCLw34.a */; };
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; };
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
@@ -573,8 +573,8 @@
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.9-J30uILApgSuBRyrol6CHua-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.9-J30uILApgSuBRyrol6CHua-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.9-J30uILApgSuBRyrol6CHua.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.9-J30uILApgSuBRyrol6CHua.a"; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.10-Fr7Tzkw2TAVKpbtViCLw34-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.10-Fr7Tzkw2TAVKpbtViCLw34-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.10-Fr7Tzkw2TAVKpbtViCLw34.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.10-Fr7Tzkw2TAVKpbtViCLw34.a"; sourceTree = "<group>"; };
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
@@ -745,8 +745,8 @@
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.9-J30uILApgSuBRyrol6CHua-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.9-J30uILApgSuBRyrol6CHua.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.10-Fr7Tzkw2TAVKpbtViCLw34-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.10-Fr7Tzkw2TAVKpbtViCLw34.a in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -832,8 +832,8 @@
64C829992D54AEEE006B9E89 /* libffi.a */,
64C829982D54AEED006B9E89 /* libgmp.a */,
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.9-J30uILApgSuBRyrol6CHua-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.9-J30uILApgSuBRyrol6CHua.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.10-Fr7Tzkw2TAVKpbtViCLw34-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.10-Fr7Tzkw2TAVKpbtViCLw34.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -2091,7 +2091,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 342;
CURRENT_PROJECT_VERSION = 343;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -2141,7 +2141,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 342;
CURRENT_PROJECT_VERSION = 343;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -2183,7 +2183,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 342;
CURRENT_PROJECT_VERSION = 343;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@@ -2203,7 +2203,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 342;
CURRENT_PROJECT_VERSION = 343;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@@ -2228,7 +2228,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 342;
CURRENT_PROJECT_VERSION = 343;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -2265,7 +2265,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 342;
CURRENT_PROJECT_VERSION = 343;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -2302,7 +2302,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 342;
CURRENT_PROJECT_VERSION = 343;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2353,7 +2353,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 342;
CURRENT_PROJECT_VERSION = 343;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2407,7 +2407,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 342;
CURRENT_PROJECT_VERSION = 343;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2441,7 +2441,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 342;
CURRENT_PROJECT_VERSION = 343;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1963,7 +1963,7 @@ fun BoxScope.ChatItemsList(
}
false
}
val swipeableModifier = if (appPlatform.isDesktop || !chatInfo.sendMsgEnabled) Modifier else SwipeToDismissModifier(
val swipeableModifier = if (appPlatform.isDesktop || !chatInfo.sendMsgEnabled || cItem.meta.itemDeleted != null) Modifier else SwipeToDismissModifier(
state = dismissState,
directions = setOf(DismissDirection.EndToStart),
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
@@ -25,6 +25,7 @@ import chat.simplex.common.views.chat.chatViewScrollState
import dev.icerock.moko.resources.StringResource
import java.io.File
import java.net.URI
import kotlin.math.roundToInt
@Composable
fun CIVideoView(
@@ -38,12 +39,25 @@ fun CIVideoView(
receiveFile: (Long) -> Unit
) {
val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }
val preview = remember(image) { base64ToBitmap(image) }
Box(
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID)
.then(
if (!smallView) {
val w = if (preview.width * 0.97 <= preview.height) videoViewFullWidth(LocalWindowWidth()) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH
// Size the media box from the preview aspect ratio (as CIImageView does), else the unprepared player surface
// expands to PriorityLayout's max height and shows as a black strip; height tracks the clamped width (#7223).
Modifier.width(w).layout { measurable, constraints ->
val width = constraints.maxWidth.coerceAtMost(w.roundToPx().coerceAtLeast(0))
val height = (width * (preview.height.toFloat() / preview.width.toFloat()).coerceAtMost(2.33f)).roundToInt().coerceAtMost(constraints.maxHeight)
val placeable = measurable.measure(Constraints.fixed(width, height))
layout(width, height) { placeable.place(0, 0) }
}
} else Modifier
)
.desktopModifyBlurredState(!smallView, blurred, showMenu),
contentAlignment = Alignment.TopEnd
) {
val preview = remember(image) { base64ToBitmap(image) }
val filePath = remember(file, CIFile.cachedRemoteFileRequests.toList()) { mutableStateOf(getLoadedFilePath(file)) }
val sizeMultiplier = if (smallView) 0.38f else 1f
if (chatModel.connectedToRemote()) {
@@ -367,7 +367,7 @@ private fun SMPSubscriptionsSection(subs: SMPServerSubs, summary: SMPServerSumma
)
SubscriptionStatusIndicatorView(subs, summary.sessionsOrNew.hasSess)
}
Column(Modifier.padding(PaddingValues()).fillMaxWidth()) {
SectionView {
InfoRow(
generalGetString(MR.strings.servers_info_subscriptions_connections_subscribed),
numOrDash(subs.ssActive)
@@ -540,20 +540,16 @@ fun XFTPServerSummaryLayout(summary: XFTPServerSummary, statsStartedAt: Instant,
)
)
}
if (summary.stats != null || summary.sessions != null) {
SectionDividerSpaced()
}
}
if (summary.stats != null) {
XFTPStatsView(stats = summary.stats, rh = rh, statsStartedAt = statsStartedAt)
if (summary.sessions != null) {
SectionDividerSpaced()
}
}
if (summary.stats != null) {
SectionDividerSpaced()
XFTPStatsView(stats = summary.stats, rh = rh, statsStartedAt = statsStartedAt)
}
if (summary.sessions != null) {
ServerSessionsView(summary.sessions)
}
if (summary.sessions != null) {
SectionDividerSpaced()
ServerSessionsView(summary.sessions)
}
SectionBottomSpacer()
@@ -572,27 +568,21 @@ fun SMPServerSummaryLayout(summary: SMPServerSummary, statsStartedAt: Instant, r
)
)
}
if (summary.stats != null || summary.subs != null || summary.sessions != null) {
SectionDividerSpaced()
}
}
if (summary.stats != null) {
SMPStatsView(stats = summary.stats, remoteHostInfo = rh, statsStartedAt = statsStartedAt)
if (summary.subs != null || summary.sessions != null) {
SectionDividerSpaced()
}
}
if (summary.stats != null) {
SectionDividerSpaced()
SMPStatsView(stats = summary.stats, remoteHostInfo = rh, statsStartedAt = statsStartedAt)
}
if (summary.subs != null) {
SMPSubscriptionsSection(subs = summary.subs, summary = summary, rh = rh)
if (summary.sessions != null) {
SectionDividerSpaced()
}
}
if (summary.subs != null) {
SectionDividerSpaced()
SMPSubscriptionsSection(subs = summary.subs, summary = summary, rh = rh)
}
if (summary.sessions != null) {
ServerSessionsView(summary.sessions)
}
if (summary.sessions != null) {
SectionDividerSpaced()
ServerSessionsView(summary.sessions)
}
SectionBottomSpacer()
@@ -196,7 +196,6 @@ private fun MutableState<MigrationFromState>.ChatStopInProgressView() {
private fun MutableState<MigrationFromState>.ChatStopFailedView(reason: String) {
SectionView(stringResource(MR.strings.error_stopping_chat)) {
Text(reason)
SectionDividerSpaced()
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_report_filled),
text = stringResource(MR.strings.auth_stop_chat),
@@ -147,8 +147,11 @@ private fun ConnectDesktop(deviceName: String, remoteCtrls: SnapshotStateList<Re
private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) {
AppBarTitle(stringResource(MR.strings.connecting_to_desktop))
SectionView(stringResource(MR.strings.connecting_to_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
CtrlDeviceNameText(session, rc)
CtrlDeviceVersionText(session)
Column {
CtrlDeviceNameText(session, rc)
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
CtrlDeviceVersionText(session)
}
}
if (session.sessionCode != null) {
@@ -223,10 +226,12 @@ private fun FoundDesktop(
}
SectionDividerSpaced()
SectionView(stringResource(MR.strings.found_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
CtrlDeviceNameText(session, rc)
CtrlDeviceVersionText(session)
if (!compatible) {
Text(stringResource(MR.strings.not_compatible), color = MaterialTheme.colors.error)
Column {
CtrlDeviceNameText(session, rc)
CtrlDeviceVersionText(session)
if (!compatible) {
Text(stringResource(MR.strings.not_compatible), color = MaterialTheme.colors.error)
}
}
}
@@ -255,8 +260,11 @@ private fun FoundDesktop(
private fun VerifySession(session: RemoteCtrlSession, rc: RemoteCtrlInfo?, sessCode: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) {
AppBarTitle(stringResource(MR.strings.verify_connection))
SectionView(stringResource(MR.strings.connected_to_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
CtrlDeviceNameText(session, rc)
CtrlDeviceVersionText(session)
Column {
CtrlDeviceNameText(session, rc)
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
CtrlDeviceVersionText(session)
}
}
SectionDividerSpaced()
@@ -308,8 +316,11 @@ private fun CtrlDeviceVersionText(session: RemoteCtrlSession) {
private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo, close: () -> Unit) {
AppBarTitle(stringResource(MR.strings.connected_to_desktop))
SectionView(stringResource(MR.strings.connected_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(rc.deviceViewName)
CtrlDeviceVersionText(session)
Column {
Text(rc.deviceViewName)
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
CtrlDeviceVersionText(session)
}
}
if (session.sessionCode != null) {
@@ -561,7 +561,6 @@ object AppearanceScope {
color = if (chatModel.remoteHostId != null && themeUserDestination.value != null) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
)
}
SectionDividerSpaced()
}
val state: State<DefaultThemeMode?> = remember(appPrefs.currentTheme.get()) {
+4 -4
View File
@@ -24,13 +24,13 @@ android.nonTransitiveRClass=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=7.0-beta.4
android.version_code=363
android.version_name=7.0-beta.5
android.version_code=364
android.bundle=false
desktop.version_name=7.0-beta.4
desktop.version_code=152
desktop.version_name=7.0-beta.5
desktop.version_code=153
kotlin.version=2.1.20
gradle.plugin.version=8.7.0
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "simplex-chat",
"version": "7.0.0-beta.4",
"version": "7.0.0-beta.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
@@ -4,7 +4,7 @@ const path = require('path');
const extract = require('extract-zip');
const GITHUB_REPO = 'simplex-chat/simplex-chat-libs';
const RELEASE_TAG = 'v7.0.0-beta.4';
const RELEASE_TAG = 'v7.0.0-beta.5';
const BACKEND = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || 'sqlite').toLowerCase();
if (BACKEND !== 'sqlite' && BACKEND !== 'postgres') {
@@ -5,5 +5,5 @@ Bump both together for normal releases. For wrapper-only fixes use a PEP 440
post-release: __version__ = "6.5.2.post1", LIBS_VERSION unchanged.
"""
__version__ = "7.0.0b4" # PEP 440 — read by hatchling for wheel metadata
LIBS_VERSION = "7.0.0-beta.4" # simplex-chat-libs release tag (no 'v' prefix)
__version__ = "7.0.0b5" # PEP 440 — read by hatchling for wheel metadata
LIBS_VERSION = "7.0.0-beta.5" # simplex-chat-libs release tag (no 'v' prefix)
@@ -0,0 +1,40 @@
# Fix stray card dividers in server info, connect-to-desktop, appearance and migration screens
Branch: `nd/fix-ui-lines` · base: `master`.
## Problem
On several card screens (Android and desktop, current `master`), near-black horizontal lines appear inside section cards where they don't belong — most visible in dark/black themes:
- **Servers info → tap an SMP or XFTP server**: the server-detail card is sliced by stray lines around its nested stats / subscriptions / sessions blocks.
- **Use from desktop → "Connected to desktop"** (also the connecting / found / verify states): two black lines between the desktop name and its version (e.g. "Desktop" and "v7.0.0.1").
- **Settings → Appearance** with a custom image wallpaper: a double line with a gap between "Remove image" and "Color mode".
- **Migrate from this device**, "error stopping chat" state: a double line between the error text and the stop-chat action.
Behaviour is correct everywhere; the bugs are purely visual.
## Cause
PR #6777 (`df5ea3d46`, "android, desktop: new settings section design") wrapped `SectionView`'s content in `CardColumnLayout`, which draws a 2 dp divider (`canvasColorForCurrentTheme()`, near-black in dark/black themes) at the bottom of every direct child except the last. The same PR turned these screens into card screens (`ModalView(cardScreen = true)`) but left three content patterns that pre-date the card chrome, each of which now produces stray lines:
1. **Nested `SectionView`s** inside one outer `SectionView` (server detail) → card-in-card, plus a line around every nested block and spacer.
2. **Loose `Text` + `Spacer` + `Text`** as separate children of a card `SectionView` (connect-to-desktop name + version) → a line after the text and after the spacer.
3. **`SectionDividerSpaced()` as a middle child** (Appearance, migration) → on a card screen this is a 30 dp `Spacer`, so it gets a line above *and* below = a double line bracketing a gap.
This is the same bug class as `2026-05-25-fix-e2e-encryption-section-divider.md` (#7012); #6777 reintroduced further instances that earlier fix did not cover.
## Fix
`apps/multiplatform/common/...`, layout-only, matching the reference sibling pattern already used by `DetailedSMPStatsLayout`:
- **`ServersSummaryView.kt`** — `SMPServerSummaryLayout` / `XFTPServerSummaryLayout`: stop wrapping everything in one outer `SectionView`; make the address its own card and each sub-section a top-level sibling separated by `SectionDividerSpaced()` placed *between* them. `SMPSubscriptionsSection`'s rows are wrapped in their own `SectionView` so they keep card chrome.
- **`ConnectDesktopView.kt`** — wrap each device-name + version block in a single `Column` (4 states), so the card sees one child and draws no internal divider.
- **`Appearance.kt`** / **`MigrateFromDevice.kt`** — delete the in-card `SectionDividerSpaced()`; the auto-divider between the two now-adjacent rows already separates them (the #6777 precedent for in-card spacers).
## Risk
- Layout-only: no logic, state, side-effect, or click-handler change.
- iOS is a separate codebase (native grouped lists) and is unaffected.
- Verified: `:common:compileKotlinDesktop` and the full desktop AppImage build succeed, and the running app renders the screens correctly. Independent adversarial review confirmed every conditional branch and separator is preserved across all stats/subs/sessions combinations.
- Deliberately out of scope: `SectionTextFooter`-inside-card instances (NetworkAndServers, Migrate*) — these render as a single normal row-separator above an in-card caption, not the stray-line bug; a separate #6777-style cleanup.
- Rollback: `git revert` the fix commit.
@@ -0,0 +1,70 @@
# Fix: video preview expands into a long empty black area
## Problem
On Android, once a video message finishes downloading, its in-chat preview grows
into a tall empty black area below the thumbnail. It appears the moment the file
download completes (before playback), and is worst for landscape videos.
## Root cause
The video message renders into the shared `CHAT_IMAGE_LAYOUT_ID` box measured by
`PriorityLayout` (`FramedItemView.kt`). Unlike `CIImageView`, `CIVideoView` never
gave that box a definite size — it inherited the height of its tallest child.
Once downloaded, the tallest child is the player surface. On Android that is a
`StyledPlayerView` with `RESIZE_MODE_FIXED_WIDTH`. Before playback the player is
unprepared (the media source is only set in `VideoPlayer.start()`, called from
`play()`), so it reports no video size and its `AspectRatioFrameLayout` keeps
aspect ratio 0 and does **not** shrink its height.
This was latent until #6726 ("constrain image sizes for previews"). That change
bounded the box to `maxHeight = 2.33 × width` to tame extreme image aspect
ratios. Before it, the box was measured with unbounded height, so on Android the
unprepared surface collapsed to 0 (`getDefaultSize` with an `UNSPECIFIED` spec)
and contributed no height — accidentally masking the missing-size flaw. After
#6726 the surface expands to fill the now-bounded `2.33 × width`, showing as a
long black strip behind the shorter thumbnail.
The later image fixes (#7123 / #7125 / #7223) all touched `CIImageView` only, so
video was never given a definite box size and kept the defect.
## Fix
Give `CIVideoView`'s `CHAT_IMAGE_LAYOUT_ID` box a definite size derived from the
preview aspect ratio, mirroring `CIImageView`. Height is computed from the
*measured* (clamped) width via `Modifier.width(w).layout { … }`, not the nominal
width, so a wide video whose nominal `w = DEFAULT_MAX_IMAGE_WIDTH` (500dp) is
clamped on a narrower screen does not leave an empty strip below — the same
correction #7223 applied to images. Applied only for `!smallView`; the small
chat-list thumbnail (`smallView = true`) keeps its existing sizing.
With the box sized to the preview footprint, the player surface can no longer
expand past it (its bounded max height now equals the thumbnail height), and the
opaque preview drawn on top exactly covers it. When playing, `RESIZE_MODE_FIXED_WIDTH`
conforms the surface to the real video aspect, which matches the thumbnail.
## Alternatives considered
- **Force the Android `StyledPlayerView` aspect from the preview.** Android-only;
treats one render backend and leaves the box height still child-dependent — a
workaround at the wrong layer.
- **Only mount `PlayerView` while playing.** Larger behavioural change; risks the
play/preview transition blinking the code already guards against; does not
establish the container invariant.
- **`matchParentSize()` on the player.** Breaks: when playing, the preview is
removed, so no sibling defines the box and it collapses to 0.
- **Naive `Modifier.width(w).height(w * ratio)`.** Reintroduces the exact #7223
empty-strip bug for wide videos on screens narrower than 500dp.
The chosen fix restores the invariant "a media chat item occupies exactly the
media's aspect-ratio footprint" at the correct layer (the media container), for
all render states (pre-download / downloaded-idle / playing / encrypted) and both
platforms, using the same aspect source the player conforms to.
## Verification
- `android:assembleDebug` (arm64 debug APK) — BUILD SUCCESSFUL.
- Desktop `createDistributable` + AppImage (Linux x86_64) — BUILD SUCCESSFUL.
- Control APK built from the same base without the fix, to A/B the black area
against the fixed build (both `7.0-beta.4`, differing only by this change).