Merge branch 'master' into sh/namespace

This commit is contained in:
Evgeny @ SimpleX Chat
2026-06-26 21:54:42 +00:00
14 changed files with 200 additions and 84 deletions
+32 -26
View File
@@ -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)
}
+18 -18
View File
@@ -183,8 +183,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.5-3gtbZllPEb75FgChAFCqhx-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.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 */; };
@@ -563,8 +563,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.5-3gtbZllPEb75FgChAFCqhx-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx.a"; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.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>"; };
@@ -735,8 +735,8 @@
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -822,8 +822,8 @@
64C829992D54AEEE006B9E89 /* libffi.a */,
64C829982D54AEED006B9E89 /* libgmp.a */,
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -2081,7 +2081,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 338;
CURRENT_PROJECT_VERSION = 339;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -2131,7 +2131,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 338;
CURRENT_PROJECT_VERSION = 339;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -2173,7 +2173,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 338;
CURRENT_PROJECT_VERSION = 339;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@@ -2193,7 +2193,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 338;
CURRENT_PROJECT_VERSION = 339;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@@ -2218,7 +2218,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 338;
CURRENT_PROJECT_VERSION = 339;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -2255,7 +2255,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 338;
CURRENT_PROJECT_VERSION = 339;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -2292,7 +2292,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 338;
CURRENT_PROJECT_VERSION = 339;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2343,7 +2343,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 338;
CURRENT_PROJECT_VERSION = 339;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2397,7 +2397,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 338;
CURRENT_PROJECT_VERSION = 339;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2431,7 +2431,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 338;
CURRENT_PROJECT_VERSION = 339;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1607,7 +1607,7 @@ object ChatController {
suspend fun apiPrepareContact(rh: Long?, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData): Chat? {
val userId = try { currentUserId("apiPrepareContact") } catch (e: Exception) { return null }
val r = sendCmd(rh, CC.APIPrepareContact(userId, connLink, contactShortLinkData))
if (r is API.Result && r.res is CR.NewPreparedChat) return r.res.chat
if (r is API.Result && r.res is CR.NewPreparedChat) return if (rh == null) r.res.chat else r.res.chat.copy(remoteHostId = rh)
Log.e(TAG, "apiPrepareContact bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_contact), "${r.responseType}: ${r.details}")
return null
@@ -1616,7 +1616,7 @@ object ChatController {
suspend fun apiPrepareGroup(rh: Long?, connLink: CreatedConnLink, directLink: Boolean, groupShortLinkData: GroupShortLinkData): Chat? {
val userId = try { currentUserId("apiPrepareGroup") } catch (e: Exception) { return null }
val r = sendCmd(rh, CC.APIPrepareGroup(userId, connLink, directLink, groupShortLinkData))
if (r is API.Result && r.res is CR.NewPreparedChat) return r.res.chat
if (r is API.Result && r.res is CR.NewPreparedChat) return if (rh == null) r.res.chat else r.res.chat.copy(remoteHostId = rh)
Log.e(TAG, "apiPrepareGroup bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_group), "${r.responseType}: ${r.details}")
return null
@@ -2172,8 +2172,8 @@ object ChatController {
return null
}
suspend fun apiGetGroupRelays(groupId: Long): List<GroupRelay> {
val r = sendCmd(null, CC.ApiGetGroupRelays(groupId))
suspend fun apiGetGroupRelays(rh: Long?, groupId: Long): List<GroupRelay> {
val r = sendCmd(rh, CC.ApiGetGroupRelays(groupId))
if (r is API.Result && r.res is CR.GroupRelays) return r.res.groupRelays
return emptyList()
}
@@ -2183,8 +2183,8 @@ object ChatController {
data class AddFailed(val addRelayResults: List<AddRelayResult>): AddGroupRelaysResult()
}
suspend fun apiAddGroupRelays(groupId: Long, relayIds: List<Long>): AddGroupRelaysResult? {
val r = sendCmdWithRetry(null, CC.ApiAddGroupRelays(groupId, relayIds))
suspend fun apiAddGroupRelays(rh: Long?, groupId: Long, relayIds: List<Long>): AddGroupRelaysResult? {
val r = sendCmdWithRetry(rh, CC.ApiAddGroupRelays(groupId, relayIds))
if (r is API.Result && r.res is CR.GroupRelaysAdded) return AddGroupRelaysResult.Added(r.res.groupInfo, r.res.groupLink, r.res.groupRelays)
if (r is API.Result && r.res is CR.GroupRelaysAddFailed) return AddGroupRelaysResult.AddFailed(r.res.addRelayResults)
if (r != null) throw Exception("${r.responseType}: ${r.details}")
@@ -208,7 +208,7 @@ fun ChatView(
withBGApi {
setGroupMembers(chatRh, cInfo.groupInfo, chatModel)
if (cInfo.groupInfo.membership.memberRole == GroupMemberRole.Owner) {
val relays = chatModel.controller.apiGetGroupRelays(cInfo.groupInfo.groupId)
val relays = chatModel.controller.apiGetGroupRelays(chatRh, cInfo.groupInfo.groupId)
withContext(Dispatchers.Main) {
ChannelRelaysModel.set(cInfo.groupInfo.groupId, relays)
}
@@ -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)
) {
@@ -31,6 +31,7 @@ data class AvailableRelay(
@Composable
fun AddGroupRelayView(
rhId: Long?,
groupInfo: GroupInfo,
existingRelayIds: Set<Long>,
onRelayAdded: () -> Unit,
@@ -46,7 +47,7 @@ fun AddGroupRelayView(
LaunchedEffect(Unit) {
try {
val servers = ChatController.getUserServers(null)
val servers = ChatController.getUserServers(rhId)
if (servers != null) {
val relays = mutableListOf<AvailableRelay>()
for (op in servers) {
@@ -80,7 +81,7 @@ fun AddGroupRelayView(
if (relayIds.isEmpty()) return@AddGroupRelayLayout
isAdding = true
scope.launch {
addSelectedRelays(groupInfo, relayIds, selectedRelayIds, availableRelays, onRelayAdded, close) { newSelectedIds, newAvailableRelays ->
addSelectedRelays(rhId, groupInfo, relayIds, selectedRelayIds, availableRelays, onRelayAdded, close) { newSelectedIds, newAvailableRelays ->
selectedRelayIds = newSelectedIds
availableRelays = newAvailableRelays
isAdding = false
@@ -183,6 +184,7 @@ private fun AddRelaysButton(onClick: () -> Unit, disabled: Boolean) {
}
private suspend fun addSelectedRelays(
rhId: Long?,
groupInfo: GroupInfo,
relayIds: List<Long>,
selectedRelayIds: Set<Long>,
@@ -192,7 +194,7 @@ private suspend fun addSelectedRelays(
updateState: (Set<Long>, List<AvailableRelay>) -> Unit
) {
try {
val result = ChatController.apiAddGroupRelays(groupInfo.groupId, relayIds)
val result = ChatController.apiAddGroupRelays(rhId, groupInfo.groupId, relayIds)
if (result == null) {
updateState(selectedRelayIds, availableRelays)
return
@@ -37,7 +37,7 @@ fun ChannelRelaysView(
LaunchedEffect(Unit) {
setGroupMembers(rhId, groupInfo, chatModel)
if (groupInfo.isOwner) {
val relays = chatModel.controller.apiGetGroupRelays(groupInfo.groupId)
val relays = chatModel.controller.apiGetGroupRelays(rhId, groupInfo.groupId)
ChannelRelaysModel.set(groupId = groupInfo.groupId, groupRelays = relays)
}
}
@@ -114,6 +114,7 @@ private fun ChannelRelaysLayout(
val existingRelayIds = groupRelays.mapNotNull { it.userChatRelay.chatRelayId }.toSet()
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close ->
AddGroupRelayView(
rhId = rhId,
groupInfo = groupInfo,
existingRelayIds = existingRelayIds,
onRelayAdded = { withBGApi { setGroupMembers(rhId, groupInfo, chatModel) } },
@@ -81,7 +81,7 @@ fun ChannelWebPageView(
}
LaunchedEffect(Unit) {
val relays = chatModel.controller.apiGetGroupRelays(groupInfo.groupId)
val relays = chatModel.controller.apiGetGroupRelays(rhId, groupInfo.groupId)
groupRelays.clear()
groupRelays.addAll(relays)
}
@@ -38,7 +38,8 @@ import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.*
@Composable
fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit) {
fun AddChannelView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, closeAll: () -> Unit) {
val rhId = rh?.remoteHostId
val view = LocalMultiplatformView()
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
@@ -56,7 +57,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
val gInfo = groupInfo.value
if (showLinkStep.value && gInfo != null) {
LinkStepView(chatModel, gInfo, groupLink, closeAll)
LinkStepView(chatModel, rhId, gInfo, groupLink, closeAll)
} else if (gInfo != null) {
ProgressStepView(
chatModel, gInfo, groupRelays, relayListExpanded,
@@ -65,9 +66,9 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
chatModel.creatingChannelId.value = null
closeAll()
withBGApi {
openGroupChat(null, gInfo.groupId)
openGroupChat(rhId, gInfo.groupId)
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close ->
GroupLinkView(chatModel, rhId = null, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = null, creatingGroup = true, isChannel = true, shareGroupInfo = gInfo, close = close)
GroupLinkView(chatModel, rhId = rhId, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = null, creatingGroup = true, isChannel = true, shareGroupInfo = gInfo, close = close)
}
}
}
@@ -80,9 +81,9 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
closeAll()
withBGApi {
try {
chatModel.controller.apiDeleteChat(rh = null, type = ChatType.Group, id = gInfo.apiId)
chatModel.controller.apiDeleteChat(rh = rhId, type = ChatType.Group, id = gInfo.apiId)
withContext(Dispatchers.Main) {
chatModel.chatsContext.removeChat(null, gInfo.id)
chatModel.chatsContext.removeChat(rhId, gInfo.id)
}
} catch (e: Exception) {
Log.e(TAG, "cancelChannelCreation error: ${e.message}")
@@ -93,6 +94,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
} else {
ProfileStepView(
chatModel = chatModel,
rhId = rhId,
displayName = displayName,
profileImage = profileImage,
chosenImage = chosenImage,
@@ -120,7 +122,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
creationInProgress.value = true
withBGApi {
try {
val enabledRelays = chooseRandomRelays()
val enabledRelays = chooseRandomRelays(rhId)
val relayIds = enabledRelays.mapNotNull { it.chatRelayId }
if (relayIds.isEmpty()) {
withContext(Dispatchers.Main) {
@@ -130,7 +132,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
return@withBGApi
}
val result = chatModel.controller.apiNewPublicGroup(
rh = null,
rh = rhId,
incognito = false,
relayIds = relayIds,
groupProfile = profile
@@ -138,7 +140,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
when (result) {
is ChatController.PublicGroupCreationResult.Created -> {
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateGroup(rhId = null, result.groupInfo)
chatModel.chatsContext.updateGroup(rhId = rhId, result.groupInfo)
chatModel.creatingChannelId.value = result.groupInfo.id
groupInfo.value = result.groupInfo
groupLink.value = result.groupLink
@@ -178,8 +180,8 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
private const val maxRelays = 3
private suspend fun chooseRandomRelays(): List<UserChatRelay> {
val servers = getUserServers(rh = null) ?: return emptyList()
private suspend fun chooseRandomRelays(rhId: Long?): List<UserChatRelay> {
val servers = getUserServers(rh = rhId) ?: return emptyList()
// Operator relays are grouped per operator; custom relays (null operator)
// are treated independently to maximize trust distribution.
val operatorGroups = mutableListOf<List<UserChatRelay>>()
@@ -215,8 +217,8 @@ private suspend fun chooseRandomRelays(): List<UserChatRelay> {
return selected
}
private suspend fun checkHasRelays(): Boolean {
val servers = try { getUserServers(rh = null) } catch (_: Exception) { null } ?: return false
private suspend fun checkHasRelays(rhId: Long?): Boolean {
val servers = try { getUserServers(rh = rhId) } catch (_: Exception) { null } ?: return false
return servers.any { op ->
(op.operator?.enabled ?: true) &&
op.chatRelays.any { it.enabled && !it.deleted && it.chatRelayId != null }
@@ -226,6 +228,7 @@ private suspend fun checkHasRelays(): Boolean {
@Composable
private fun ProfileStepView(
chatModel: ChatModel,
rhId: Long?,
displayName: MutableState<String>,
profileImage: MutableState<String?>,
chosenImage: MutableState<URI?>,
@@ -239,7 +242,7 @@ private fun ProfileStepView(
createChannel: () -> Unit
) {
LaunchedEffect(Unit) {
hasRelays.value = checkHasRelays()
hasRelays.value = checkHasRelays(rhId)
}
ModalBottomSheetLayout(
@@ -553,6 +556,7 @@ private fun RelayRow(relay: GroupRelay, connFailed: Boolean) {
@Composable
private fun LinkStepView(
chatModel: ChatModel,
rhId: Long?,
gInfo: GroupInfo,
groupLink: MutableState<GroupLink?>,
closeAll: () -> Unit
@@ -563,14 +567,14 @@ private fun LinkStepView(
delay(500)
withContext(Dispatchers.Main) {
ModalManager.start.closeModals()
openGroupChat(null, gInfo.groupId)
openGroupChat(rhId, gInfo.groupId)
}
}
}
ModalView(close = close, showClose = false, cardScreen = true) {
GroupLinkView(
chatModel = chatModel,
rhId = null,
rhId = rhId,
groupInfo = gInfo,
groupLink = groupLink.value,
onGroupLinkUpdated = { groupLink.value = it },
@@ -660,6 +664,6 @@ fun RelayProgressIndicator(active: Int, total: Int) {
@Composable
fun PreviewAddChannelView() {
SimpleXTheme {
AddChannelView(chatModel = ChatModel, close = {}, closeAll = {})
AddChannelView(chatModel = ChatModel, rh = null, close = {}, closeAll = {})
}
}
@@ -64,7 +64,7 @@ fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) }
},
createChannel = {
ModalManager.start.showCustomModal { close -> AddChannelView(chatModel, close, closeAll) }
ModalManager.start.showCustomModal { close -> AddChannelView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) }
},
rh = rh,
close = close
+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.1
android.version_code=360
android.version_name=7.0-beta.2
android.version_code=361
android.bundle=false
desktop.version_name=7.0-beta.1
desktop.version_code=149
desktop.version_name=7.0-beta.2
desktop.version_code=150
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.1",
"version": "7.0.0-beta.2",
"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.1';
const RELEASE_TAG = 'v7.0.0-beta.2';
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.0b1" # PEP 440 — read by hatchling for wheel metadata
LIBS_VERSION = "7.0.0-beta.1" # simplex-chat-libs release tag (no 'v' prefix)
__version__ = "7.0.0b2" # PEP 440 — read by hatchling for wheel metadata
LIBS_VERSION = "7.0.0-beta.2" # simplex-chat-libs release tag (no 'v' prefix)
@@ -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.