mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-16 19:41:57 +00:00
Merge branch 'stable'
This commit is contained in:
@@ -1404,6 +1404,7 @@ enum GroupLinkPlan: Decodable, Hashable {
|
||||
case connectingProhibit(groupInfo_: GroupInfo?)
|
||||
case known(groupInfo: GroupInfo)
|
||||
case noRelays(groupSLinkData_: GroupShortLinkData?)
|
||||
case updateRequired(groupSLinkData_: GroupShortLinkData?)
|
||||
}
|
||||
|
||||
struct ChatTagData: Encodable {
|
||||
|
||||
@@ -208,7 +208,9 @@ private func handleTextTaps(
|
||||
var browser: Bool = false
|
||||
s.enumerateAttributes(in: NSRange(location: 0, length: s.length)) { attrs, range, stop in
|
||||
if index >= range.location && index < range.location + range.length {
|
||||
if let url = attrs[linkAttrKey] as? String {
|
||||
if let nameInfo = attrs[nameAttrKey] as? SimplexNameInfo {
|
||||
showUnsupportedNameAlert(nameInfo)
|
||||
} else if let url = attrs[linkAttrKey] as? String {
|
||||
linkURL = url
|
||||
browser = attrs[webLinkAttrKey] != nil
|
||||
} else if let showSecrets, let i = attrs[secretAttrKey] as? Int {
|
||||
@@ -251,6 +253,7 @@ private let webLinkAttrKey = NSAttributedString.Key("chat.simplex.app.webLink")
|
||||
private let secretAttrKey = NSAttributedString.Key("chat.simplex.app.secret")
|
||||
|
||||
private let commandAttrKey = NSAttributedString.Key("chat.simplex.app.command")
|
||||
private let nameAttrKey = NSAttributedString.Key("chat.simplex.app.name")
|
||||
|
||||
typealias MsgTextResult = (string: NSMutableAttributedString, hasSecrets: Bool, handleTaps: Bool)
|
||||
|
||||
@@ -424,6 +427,12 @@ func messageText(
|
||||
t = mentionText(memberName)
|
||||
}
|
||||
}
|
||||
case let .simplexName(nameInfo):
|
||||
attrs = linkAttrs()
|
||||
if !preview {
|
||||
attrs[nameAttrKey] = nameInfo
|
||||
handleTaps = true
|
||||
}
|
||||
case .email:
|
||||
attrs = linkAttrs()
|
||||
if !preview {
|
||||
|
||||
@@ -675,17 +675,18 @@ struct ChatListSearchBar: View {
|
||||
if ignoreSearchTextChange {
|
||||
ignoreSearchTextChange = false
|
||||
} else {
|
||||
if let link = strHasSingleSimplexLink(t.trimmingCharacters(in: .whitespaces)) { // if SimpleX link is pasted, show connection dialogue
|
||||
switch strConnectTarget(t.trimmingCharacters(in: .whitespaces)) {
|
||||
case let .link(text, _, linkText):
|
||||
searchFocussed = false
|
||||
if case let .simplexLink(_, linkType, _, smpHosts) = link.format {
|
||||
ignoreSearchTextChange = true
|
||||
searchText = simplexLinkText(linkType, smpHosts)
|
||||
}
|
||||
ignoreSearchTextChange = true
|
||||
searchText = linkText
|
||||
searchShowingSimplexLink = true
|
||||
searchChatFilteredBySimplexLink = nil
|
||||
connect(link.text)
|
||||
} else {
|
||||
if t != "" { // if some other text is pasted, enter search mode
|
||||
connect(text)
|
||||
case let .name(nameInfo):
|
||||
showUnsupportedNameAlert(nameInfo)
|
||||
case .none:
|
||||
if t != "" {
|
||||
searchFocussed = true
|
||||
} else {
|
||||
ConnectProgressManager.shared.cancelConnectProgress()
|
||||
|
||||
@@ -381,17 +381,18 @@ struct ContactsListSearchBar: View {
|
||||
if ignoreSearchTextChange {
|
||||
ignoreSearchTextChange = false
|
||||
} else {
|
||||
if let link = strHasSingleSimplexLink(t.trimmingCharacters(in: .whitespaces)) { // if SimpleX link is pasted, show connection dialogue
|
||||
switch strConnectTarget(t.trimmingCharacters(in: .whitespaces)) {
|
||||
case let .link(text, _, linkText):
|
||||
searchFocussed = false
|
||||
if case let .simplexLink(_, linkType, _, smpHosts) = link.format {
|
||||
ignoreSearchTextChange = true
|
||||
searchText = simplexLinkText(linkType, smpHosts)
|
||||
}
|
||||
ignoreSearchTextChange = true
|
||||
searchText = linkText
|
||||
searchShowingSimplexLink = true
|
||||
searchChatFilteredBySimplexLink = nil
|
||||
connect(link.text)
|
||||
} else {
|
||||
if t != "" { // if some other text is pasted, enter search mode
|
||||
connect(text)
|
||||
case let .name(nameInfo):
|
||||
showUnsupportedNameAlert(nameInfo)
|
||||
case .none:
|
||||
if t != "" {
|
||||
searchFocussed = true
|
||||
} else {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
|
||||
@@ -663,14 +663,13 @@ private struct ConnectView: View {
|
||||
ZStack(alignment: .trailing) {
|
||||
Button {
|
||||
if let str = UIPasteboard.general.string {
|
||||
if let link = strHasSingleSimplexLink(str.trimmingCharacters(in: .whitespaces)) {
|
||||
pastedLink = link.text
|
||||
// It would be good to hide it, but right now it is not clear how to release camera in CodeScanner
|
||||
// https://github.com/twostraws/CodeScanner/issues/121
|
||||
// No known tricks worked (changing view ID, wrapping it in another view, etc.)
|
||||
// showQRCodeScanner = false
|
||||
switch strConnectTarget(str.trimmingCharacters(in: .whitespaces)) {
|
||||
case let .link(text, _, _):
|
||||
pastedLink = text
|
||||
connect(pastedLink)
|
||||
} else {
|
||||
case let .name(nameInfo):
|
||||
showUnsupportedNameAlert(nameInfo)
|
||||
case .none:
|
||||
alert = .newChatSomeAlert(alert: SomeAlert(
|
||||
alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."),
|
||||
id: "pasteLinkView: code is not a SimpleX link"
|
||||
@@ -866,16 +865,36 @@ func strIsSimplexLink(_ str: String) -> Bool {
|
||||
}
|
||||
}
|
||||
|
||||
func strHasSingleSimplexLink(_ str: String) -> FormattedText? {
|
||||
if let parsedMd = parseSimpleXMarkdown(str) {
|
||||
let parsedLinks = parsedMd.filter({ $0.format?.isSimplexLink ?? false })
|
||||
if parsedLinks.count == 1 {
|
||||
return parsedLinks[0]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
enum ConnectTarget {
|
||||
case link(text: String, linkType: SimplexLinkType, linkText: String)
|
||||
case name(SimplexNameInfo)
|
||||
}
|
||||
|
||||
func strConnectTarget(_ str: String) -> ConnectTarget? {
|
||||
let parsedMd = parseSimpleXMarkdown(str)
|
||||
let links = parsedMd?.filter { $0.format?.isSimplexLink ?? false } ?? []
|
||||
return if links.count == 1, case let .simplexLink(_, linkType, _, smpHosts) = links[0].format {
|
||||
.link(text: links[0].text, linkType: linkType, linkText: simplexLinkText(linkType, smpHosts))
|
||||
} else if links.isEmpty,
|
||||
case let .simplexName(nameInfo) = parsedMd?.first(where: { if case .simplexName = $0.format { true } else { false } })?.format {
|
||||
.name(nameInfo)
|
||||
} else {
|
||||
return nil
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
func showUnsupportedNameAlert(_ nameInfo: SimplexNameInfo) {
|
||||
let upgrade = " " + NSLocalizedString("Please upgrade the app.", comment: "alert message")
|
||||
if nameInfo.nameType == .contact {
|
||||
showAlert(
|
||||
NSLocalizedString("Unsupported contact name", comment: "alert title"),
|
||||
message: NSLocalizedString("Connecting via contact name requires a newer app version.", comment: "alert message") + upgrade
|
||||
)
|
||||
} else {
|
||||
showAlert(
|
||||
NSLocalizedString("Unsupported channel name", comment: "alert title"),
|
||||
message: NSLocalizedString("Connecting via channel name requires a newer app version.", comment: "alert message") + upgrade
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1295,13 +1314,21 @@ func planAndConnect(
|
||||
filterKnownContact: ((Contact) -> Void)? = nil,
|
||||
filterKnownGroup: ((GroupInfo) -> Void)? = nil
|
||||
) {
|
||||
if case .simplexLink(_, .relay, _, _) = strHasSingleSimplexLink(shortOrFullLink)?.format {
|
||||
showAlert(
|
||||
NSLocalizedString("Relay address", comment: "alert title"),
|
||||
message: NSLocalizedString("This is a chat relay address, it cannot be used to connect.", comment: "alert message")
|
||||
)
|
||||
switch strConnectTarget(shortOrFullLink) {
|
||||
case let .name(nameInfo):
|
||||
showUnsupportedNameAlert(nameInfo)
|
||||
cleanup?()
|
||||
return
|
||||
case let .link(_, linkType, _):
|
||||
if linkType == .relay {
|
||||
showAlert(
|
||||
NSLocalizedString("Relay address", comment: "alert title"),
|
||||
message: NSLocalizedString("This is a chat relay address, it cannot be used to connect.", comment: "alert message")
|
||||
)
|
||||
cleanup?()
|
||||
return
|
||||
}
|
||||
case .none: break
|
||||
}
|
||||
ConnectProgressManager.shared.cancelConnectProgress()
|
||||
let inProgress = BoxedValue(true)
|
||||
@@ -1559,6 +1586,33 @@ func planAndConnect(
|
||||
cleanup?()
|
||||
}
|
||||
}
|
||||
case let .updateRequired(groupSLinkData_):
|
||||
logger.debug("planAndConnect, .groupLink, .updateRequired")
|
||||
await MainActor.run {
|
||||
if let groupSLinkData = groupSLinkData_ {
|
||||
showOpenChatAlert(
|
||||
profileName: groupSLinkData.groupProfile.displayName,
|
||||
profileFullName: groupSLinkData.groupProfile.fullName,
|
||||
profileImage:
|
||||
ProfileImage(
|
||||
imageStr: groupSLinkData.groupProfile.image,
|
||||
iconName: "person.2.circle.fill",
|
||||
size: alertProfileImageSize
|
||||
),
|
||||
theme: theme,
|
||||
subtitle: NSLocalizedString("This group requires a newer version of the app. Please update the app to join.", comment: "alert subtitle"),
|
||||
cancelTitle: NSLocalizedString("OK", comment: "alert button"),
|
||||
confirmTitle: nil,
|
||||
onCancel: { cleanup?() }
|
||||
)
|
||||
} else {
|
||||
showAlert(
|
||||
NSLocalizedString("App update required", comment: "alert title"),
|
||||
message: NSLocalizedString("This group requires a newer version of the app. Please update the app to join.", comment: "alert message")
|
||||
)
|
||||
cleanup?()
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .error(chatError):
|
||||
logger.debug("planAndConnect, .error \(chatErrorString(chatError))")
|
||||
|
||||
@@ -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-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa-ghc9.6.3.a */; };
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa.a */; };
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.3.0-GwIsQfwCbNXBEEtsO80Uxy-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.3.0-GwIsQfwCbNXBEEtsO80Uxy-ghc9.6.3.a */; };
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.3.0-GwIsQfwCbNXBEEtsO80Uxy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.3.0-GwIsQfwCbNXBEEtsO80Uxy.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 */; };
|
||||
@@ -561,8 +561,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-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa.a"; sourceTree = "<group>"; };
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.3.0-GwIsQfwCbNXBEEtsO80Uxy-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.3.0-GwIsQfwCbNXBEEtsO80Uxy-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.3.0-GwIsQfwCbNXBEEtsO80Uxy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.3.0-GwIsQfwCbNXBEEtsO80Uxy.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>"; };
|
||||
@@ -731,8 +731,8 @@
|
||||
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
|
||||
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
|
||||
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa-ghc9.6.3.a in Frameworks */,
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa.a in Frameworks */,
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.3.0-GwIsQfwCbNXBEEtsO80Uxy-ghc9.6.3.a in Frameworks */,
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.3.0-GwIsQfwCbNXBEEtsO80Uxy.a in Frameworks */,
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -818,8 +818,8 @@
|
||||
64C829992D54AEEE006B9E89 /* libffi.a */,
|
||||
64C829982D54AEED006B9E89 /* libgmp.a */,
|
||||
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa-ghc9.6.3.a */,
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa.a */,
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.3.0-GwIsQfwCbNXBEEtsO80Uxy-ghc9.6.3.a */,
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.3.0-GwIsQfwCbNXBEEtsO80Uxy.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -2073,7 +2073,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 332;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -2098,7 +2098,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES_THIN;
|
||||
MARKETING_VERSION = 6.5.2;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
@@ -2123,7 +2123,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 332;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -2148,7 +2148,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.5.2;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
@@ -2165,11 +2165,11 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 332;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.5.2;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2185,11 +2185,11 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 332;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.5.2;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2210,7 +2210,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 332;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -2225,7 +2225,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.5.2;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2247,7 +2247,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 332;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -2262,7 +2262,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.5.2;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2284,7 +2284,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 332;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2310,7 +2310,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.5.2;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2335,7 +2335,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 332;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2362,7 +2362,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.5.2;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2389,7 +2389,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 332;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2404,7 +2404,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.5.2;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2423,7 +2423,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 332;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2438,7 +2438,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.5.2;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -5106,6 +5106,7 @@ public enum Format: Decodable, Equatable, Hashable {
|
||||
case uri
|
||||
case hyperLink(showText: String?, linkUri: String)
|
||||
case simplexLink(showText: String?, linkType: SimplexLinkType, simplexUri: String, smpHosts: [String])
|
||||
case simplexName(nameInfo: SimplexNameInfo)
|
||||
case command(commandStr: String)
|
||||
case mention(memberName: String)
|
||||
case email
|
||||
@@ -5140,6 +5141,24 @@ public enum SimplexLinkType: String, Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SimplexNameInfo: Decodable, Equatable, Hashable {
|
||||
public var nameType: SimplexNameType
|
||||
public var nameTLD: SimplexTLD
|
||||
public var domain: String
|
||||
public var subDomain: [String]
|
||||
}
|
||||
|
||||
public enum SimplexTLD: String, Decodable, Hashable {
|
||||
case simplex
|
||||
case testing
|
||||
case web
|
||||
}
|
||||
|
||||
public enum SimplexNameType: String, Decodable, Hashable {
|
||||
case publicGroup
|
||||
case contact
|
||||
}
|
||||
|
||||
public enum FormatColor: String, Decodable, Hashable {
|
||||
case red = "red"
|
||||
case green = "green"
|
||||
|
||||
+23
@@ -4711,6 +4711,7 @@ sealed class Format {
|
||||
val viaHosts: String get() =
|
||||
"(${String.format(generalGetString(MR.strings.simplex_link_connection), smpHosts.firstOrNull() ?: "?")})"
|
||||
}
|
||||
@Serializable @SerialName("simplexName") class SimplexName(val nameInfo: SimplexNameInfo): Format()
|
||||
@Serializable @SerialName("command") class Command(val commandStr: String): Format()
|
||||
@Serializable @SerialName("mention") class Mention(val memberName: String): Format()
|
||||
@Serializable @SerialName("email") class Email: Format()
|
||||
@@ -4728,6 +4729,7 @@ sealed class Format {
|
||||
is Uri -> linkStyle
|
||||
is HyperLink -> linkStyle
|
||||
is SimplexLink -> linkStyle
|
||||
is SimplexName -> linkStyle
|
||||
is Command -> SpanStyle(color = MaterialTheme.colors.primary, fontFamily = FontFamily.Monospace)
|
||||
is Mention -> SpanStyle(fontWeight = FontWeight.Medium)
|
||||
is Email -> linkStyle
|
||||
@@ -4759,6 +4761,27 @@ enum class SimplexLinkType(val linkType: String) {
|
||||
})
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SimplexNameInfo(
|
||||
val nameType: SimplexNameType,
|
||||
val nameTLD: SimplexTLD,
|
||||
val domain: String,
|
||||
val subDomain: List<String>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class SimplexTLD {
|
||||
@SerialName("simplex") simplex,
|
||||
@SerialName("testing") testing,
|
||||
@SerialName("web") web
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class SimplexNameType {
|
||||
@SerialName("publicGroup") publicGroup,
|
||||
@SerialName("contact") contact
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class FormatColor(val color: String) {
|
||||
red("red"),
|
||||
|
||||
+1
@@ -6994,6 +6994,7 @@ sealed class GroupLinkPlan {
|
||||
@Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val groupInfo_: GroupInfo? = null): GroupLinkPlan()
|
||||
@Serializable @SerialName("known") class Known(val groupInfo: GroupInfo): GroupLinkPlan()
|
||||
@Serializable @SerialName("noRelays") class NoRelays(val groupSLinkData_: GroupShortLinkData? = null): GroupLinkPlan()
|
||||
@Serializable @SerialName("updateRequired") class UpdateRequired(val groupSLinkData_: GroupShortLinkData? = null): GroupLinkPlan()
|
||||
}
|
||||
|
||||
abstract class TerminalItem {
|
||||
|
||||
+18
-1
@@ -281,6 +281,13 @@ fun MarkdownText (
|
||||
}
|
||||
}
|
||||
}
|
||||
is Format.SimplexName -> {
|
||||
hasLinks = true
|
||||
val ftStyle = Format.linkStyle
|
||||
withAnnotation(tag = "SIMPLEX_NAME", annotation = i.toString()) {
|
||||
withStyle(ftStyle) { append(ft.text) }
|
||||
}
|
||||
}
|
||||
is Format.Email -> {
|
||||
hasLinks = true
|
||||
val ftStyle = Format.linkStyle
|
||||
@@ -329,6 +336,16 @@ fun MarkdownText (
|
||||
withAnnotation("WEB_URL") { a -> openBrowserAlert(a.item, uriHandler) }
|
||||
withAnnotation("OTHER_URL") { a -> safeOpenUri(a.item, uriHandler) }
|
||||
withAnnotation("SIMPLEX_URL") { a -> uriHandler.openVerifiedSimplexUri(a.item) }
|
||||
withAnnotation("SIMPLEX_NAME") { a ->
|
||||
val idx = a.item.toIntOrNull()
|
||||
val nameInfo = (idx?.let { formattedText.getOrNull(it) }?.format as? Format.SimplexName)?.nameInfo
|
||||
val (title, msg) = if (nameInfo?.nameType == SimplexNameType.contact) {
|
||||
generalGetString(MR.strings.unsupported_contact_name) to generalGetString(MR.strings.contact_name_requires_newer_app_version)
|
||||
} else {
|
||||
generalGetString(MR.strings.unsupported_channel_name) to generalGetString(MR.strings.channel_name_requires_newer_app_version)
|
||||
}
|
||||
AlertManager.shared.showAlertMsg(title, "$msg ${generalGetString(MR.strings.please_upgrade_the_app)}")
|
||||
}
|
||||
}
|
||||
if (hasSecrets) {
|
||||
withAnnotation("SECRET") { a ->
|
||||
@@ -343,7 +360,7 @@ fun MarkdownText (
|
||||
onHover = { offset ->
|
||||
val hasAnnotation: (String) -> Boolean = { tag -> annotatedText.hasStringAnnotations(tag, start = offset, end = offset) }
|
||||
icon.value =
|
||||
if (hasAnnotation("WEB_URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("OTHER_URL") || hasAnnotation("SECRET") || hasAnnotation("COMMAND")) {
|
||||
if (hasAnnotation("WEB_URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("OTHER_URL") || hasAnnotation("SIMPLEX_NAME") || hasAnnotation("SECRET") || hasAnnotation("COMMAND")) {
|
||||
PointerIcon.Hand
|
||||
} else {
|
||||
PointerIcon.Text
|
||||
|
||||
+20
-22
@@ -792,31 +792,29 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState
|
||||
snapshotFlow { searchText.value.text }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
val link = strHasSingleSimplexLink(it.trim())
|
||||
if (link != null) {
|
||||
// if SimpleX link is pasted, show connection dialogue
|
||||
hideKeyboard(view)
|
||||
if (link.format is Format.SimplexLink) {
|
||||
val linkText = link.format.simplexLinkText
|
||||
searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero)
|
||||
when (val target = strConnectTarget(it.trim())) {
|
||||
is ConnectTarget.Link -> {
|
||||
hideKeyboard(view)
|
||||
searchText.value = searchText.value.copy(target.linkText, selection = TextRange.Zero)
|
||||
searchShowingSimplexLink.value = true
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
connect(target.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() }
|
||||
}
|
||||
searchShowingSimplexLink.value = true
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() }
|
||||
} else if (!searchShowingSimplexLink.value || it.isEmpty()) {
|
||||
if (it.isNotEmpty()) {
|
||||
// if some other text is pasted, enter search mode
|
||||
focusRequester.requestFocus()
|
||||
} else {
|
||||
if (!chatModel.appOpenUrlConnecting.value) {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
}
|
||||
if (listState.layoutInfo.totalItemsCount > 0) {
|
||||
listState.scrollToItem(0)
|
||||
is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo)
|
||||
null -> if (!searchShowingSimplexLink.value || it.isEmpty()) {
|
||||
if (it.isNotEmpty()) {
|
||||
focusRequester.requestFocus()
|
||||
} else {
|
||||
if (!chatModel.appOpenUrlConnecting.value) {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
}
|
||||
if (listState.layoutInfo.totalItemsCount > 0) {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
searchShowingSimplexLink.value = false
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
}
|
||||
searchShowingSimplexLink.value = false
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+46
-13
@@ -30,14 +30,23 @@ suspend fun planAndConnect(
|
||||
filterKnownContact: ((Contact) -> Unit)? = null,
|
||||
filterKnownGroup: ((GroupInfo) -> Unit)? = null,
|
||||
): CompletableDeferred<Boolean> {
|
||||
val link = strHasSingleSimplexLink(shortOrFullLink.trim())
|
||||
if (link?.format is Format.SimplexLink && (link.format as Format.SimplexLink).linkType == SimplexLinkType.relay) {
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.relay_address_alert_title),
|
||||
generalGetString(MR.strings.relay_address_alert_message),
|
||||
)
|
||||
cleanup?.invoke()
|
||||
return CompletableDeferred(false)
|
||||
when (val target = strConnectTarget(shortOrFullLink.trim())) {
|
||||
is ConnectTarget.Name -> {
|
||||
showUnsupportedNameAlert(target.nameInfo)
|
||||
cleanup?.invoke()
|
||||
return CompletableDeferred(false)
|
||||
}
|
||||
is ConnectTarget.Link -> {
|
||||
if (target.linkType == SimplexLinkType.relay) {
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.relay_address_alert_title),
|
||||
generalGetString(MR.strings.relay_address_alert_message),
|
||||
)
|
||||
cleanup?.invoke()
|
||||
return CompletableDeferred(false)
|
||||
}
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
val inProgress = mutableStateOf(true)
|
||||
@@ -73,11 +82,8 @@ private suspend fun planAndConnectTask(
|
||||
if (!inProgress.value) { return completable }
|
||||
if (result != null) {
|
||||
val (connectionLink, connectionPlan) = result
|
||||
val link = strHasSingleSimplexLink(shortOrFullLink.trim())
|
||||
val linkText = if (link?.format is Format.SimplexLink)
|
||||
"<br><br><u>${link.format.simplexLinkText}</u>"
|
||||
else
|
||||
""
|
||||
val target = strConnectTarget(shortOrFullLink.trim())
|
||||
val linkText = if (target is ConnectTarget.Link) "<br><br><u>${target.linkText}</u>" else ""
|
||||
when (connectionPlan) {
|
||||
is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) {
|
||||
is InvitationLinkPlan.Ok ->
|
||||
@@ -316,6 +322,33 @@ private suspend fun planAndConnectTask(
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
is GroupLinkPlan.UpdateRequired -> {
|
||||
Log.d(TAG, "planAndConnect, .GroupLink, .UpdateRequired")
|
||||
val groupSLinkData = connectionPlan.groupLinkPlan.groupSLinkData_
|
||||
if (groupSLinkData != null) {
|
||||
AlertManager.privacySensitive.showOpenChatAlert(
|
||||
profileName = groupSLinkData.groupProfile.displayName,
|
||||
profileFullName = groupSLinkData.groupProfile.fullName,
|
||||
profileImage = {
|
||||
ProfileImage(
|
||||
size = alertProfileImageSize,
|
||||
image = groupSLinkData.groupProfile.image,
|
||||
icon = MR.images.ic_supervised_user_circle_filled
|
||||
)
|
||||
},
|
||||
subtitle = generalGetString(MR.strings.group_link_requires_newer_version),
|
||||
confirmText = null,
|
||||
dismissText = generalGetString(MR.strings.ok),
|
||||
onDismiss = { cleanup() }
|
||||
)
|
||||
} else {
|
||||
AlertManager.privacySensitive.showAlertMsg(
|
||||
generalGetString(MR.strings.app_update_required),
|
||||
generalGetString(MR.strings.group_link_requires_newer_version)
|
||||
)
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
is ConnectionPlan.Error -> {
|
||||
Log.d(TAG, "planAndConnect, error ${connectionPlan.chatError}")
|
||||
|
||||
+23
-25
@@ -523,34 +523,32 @@ private fun ContactsSearchBar(
|
||||
snapshotFlow { searchText.value.text }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
val link = strHasSingleSimplexLink(it.trim())
|
||||
if (link != null) {
|
||||
// if SimpleX link is pasted, show connection dialogue
|
||||
hideKeyboard(view)
|
||||
if (link.format is Format.SimplexLink) {
|
||||
val linkText = link.format.simplexLinkText
|
||||
searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero)
|
||||
when (val target = strConnectTarget(it.trim())) {
|
||||
is ConnectTarget.Link -> {
|
||||
hideKeyboard(view)
|
||||
searchText.value = searchText.value.copy(target.linkText, selection = TextRange.Zero)
|
||||
searchShowingSimplexLink.value = true
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
connect(
|
||||
link = target.text,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
cleanup = { searchText.value = TextFieldValue() }
|
||||
)
|
||||
}
|
||||
searchShowingSimplexLink.value = true
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
connect(
|
||||
link = link.text,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
cleanup = { searchText.value = TextFieldValue() }
|
||||
)
|
||||
} else if (!searchShowingSimplexLink.value || it.isEmpty()) {
|
||||
if (it.isNotEmpty()) {
|
||||
// if some other text is pasted, enter search mode
|
||||
focusRequester.requestFocus()
|
||||
} else {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
if (listState.layoutInfo.totalItemsCount > 0) {
|
||||
listState.scrollToItem(0)
|
||||
is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo)
|
||||
null -> if (!searchShowingSimplexLink.value || it.isEmpty()) {
|
||||
if (it.isNotEmpty()) {
|
||||
focusRequester.requestFocus()
|
||||
} else {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
if (listState.layoutInfo.totalItemsCount > 0) {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
searchShowingSimplexLink.value = false
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
}
|
||||
searchShowingSimplexLink.value = false
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-12
@@ -671,13 +671,14 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState<String>, showQRC
|
||||
val clipboard = LocalClipboardManager.current
|
||||
SectionItemView({
|
||||
val str = clipboard.getText()?.text ?: return@SectionItemView
|
||||
val link = strHasSingleSimplexLink(str.trim())
|
||||
if (link != null) {
|
||||
pastedLink.value = link.text
|
||||
showQRCodeScanner.value = false
|
||||
withBGApi { connect(rhId, link.text, close) { pastedLink.value = "" } }
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
when (val target = strConnectTarget(str.trim())) {
|
||||
is ConnectTarget.Link -> {
|
||||
pastedLink.value = target.text
|
||||
showQRCodeScanner.value = false
|
||||
withBGApi { connect(rhId, target.text, close) { pastedLink.value = "" } }
|
||||
}
|
||||
is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo)
|
||||
null -> AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.invalid_contact_link),
|
||||
text = generalGetString(MR.strings.the_text_you_pasted_is_not_a_link)
|
||||
)
|
||||
@@ -819,12 +820,32 @@ fun strIsSimplexLink(str: String): Boolean {
|
||||
return parsedMd != null && parsedMd.size == 1 && parsedMd[0].format is Format.SimplexLink
|
||||
}
|
||||
|
||||
fun strHasSingleSimplexLink(str: String): FormattedText? {
|
||||
val parsedMd = parseToMarkdown(str) ?: return null
|
||||
val parsedLinks = parsedMd.filter { it.format?.isSimplexLink ?: false }
|
||||
if (parsedLinks.size != 1) return null
|
||||
sealed class ConnectTarget {
|
||||
class Link(val text: String, val linkType: SimplexLinkType, val linkText: String) : ConnectTarget()
|
||||
class Name(val nameInfo: SimplexNameInfo) : ConnectTarget()
|
||||
}
|
||||
|
||||
return parsedLinks[0]
|
||||
fun strConnectTarget(str: String): ConnectTarget? {
|
||||
val parsedMd = parseToMarkdown(str) ?: return null
|
||||
val links = parsedMd.filter { it.format?.isSimplexLink ?: false }
|
||||
if (links.size == 1) {
|
||||
val fmt = links[0].format as Format.SimplexLink
|
||||
return ConnectTarget.Link(links[0].text, fmt.linkType, fmt.simplexLinkText)
|
||||
}
|
||||
if (links.isEmpty()) {
|
||||
val nameInfo = parsedMd.firstNotNullOfOrNull { (it.format as? Format.SimplexName)?.nameInfo }
|
||||
if (nameInfo != null) return ConnectTarget.Name(nameInfo)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun showUnsupportedNameAlert(nameInfo: SimplexNameInfo) {
|
||||
val (title, msg) = if (nameInfo.nameType == SimplexNameType.contact) {
|
||||
generalGetString(MR.strings.unsupported_contact_name) to generalGetString(MR.strings.contact_name_requires_newer_app_version)
|
||||
} else {
|
||||
generalGetString(MR.strings.unsupported_channel_name) to generalGetString(MR.strings.channel_name_requires_newer_app_version)
|
||||
}
|
||||
AlertManager.shared.showAlertMsg(title, "$msg ${generalGetString(MR.strings.please_upgrade_the_app)}")
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -194,8 +194,15 @@
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Please check that you used the correct link or ask your contact to send you another one.</string>
|
||||
<string name="unsupported_connection_link">Unsupported connection link</string>
|
||||
<string name="link_requires_newer_app_version_please_upgrade">This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</string>
|
||||
<string name="unsupported_channel_name">Unsupported channel name</string>
|
||||
<string name="unsupported_contact_name">Unsupported contact name</string>
|
||||
<string name="channel_name_requires_newer_app_version">Connecting via channel name requires a newer app version.</string>
|
||||
<string name="contact_name_requires_newer_app_version">Connecting via contact name requires a newer app version.</string>
|
||||
<string name="please_upgrade_the_app">Please upgrade the app.</string>
|
||||
<string name="channel_temporarily_unavailable">Channel temporarily unavailable</string>
|
||||
<string name="channel_no_active_relays_try_later">Channel has no active relays. Please try to join later.</string>
|
||||
<string name="app_update_required">App update required</string>
|
||||
<string name="group_link_requires_newer_version">This group requires a newer version of the app. Please update the app to join.</string>
|
||||
<string name="connection_error_auth">Connection error (AUTH)</string>
|
||||
<string name="connection_error_auth_desc">Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection.</string>
|
||||
<string name="connection_error_blocked">Connection blocked</string>
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ val desktopPlatform = detectDesktopPlatform()
|
||||
|
||||
enum class DesktopPlatform(val libExtension: String, val configPath: String, val dataPath: String, val githubAssetName: String) {
|
||||
LINUX_X86_64("so", unixConfigPath, unixDataPath, "simplex-desktop-x86_64.AppImage"),
|
||||
LINUX_AARCH64("so", unixConfigPath, unixDataPath, " simplex-desktop-aarch64.AppImage"),
|
||||
LINUX_AARCH64("so", unixConfigPath, unixDataPath, "simplex-desktop-aarch64.AppImage"),
|
||||
WINDOWS_X86_64("dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX", "simplex-desktop-windows-x86_64.msi"),
|
||||
MAC_X86_64("dylib", unixConfigPath, unixDataPath, "simplex-desktop-macos-x86_64.dmg"),
|
||||
MAC_AARCH64("dylib", unixConfigPath, unixDataPath, "simplex-desktop-macos-aarch64.dmg");
|
||||
|
||||
+8
-6
@@ -6,6 +6,7 @@ import androidx.compose.ui.graphics.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import org.jetbrains.compose.videoplayer.SkiaBitmapVideoSurface
|
||||
import uk.co.caprica.vlcj.media.VideoOrientation
|
||||
import uk.co.caprica.vlcj.player.base.*
|
||||
import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent
|
||||
@@ -214,7 +215,7 @@ actual class VideoPlayer actual constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration = withContext(playerThread.asCoroutineDispatcher()) {
|
||||
suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration = withContext(previewThread.asCoroutineDispatcher()) {
|
||||
val mediaComponent = getOrCreateHelperPlayer()
|
||||
val player = mediaComponent.mediaPlayer()
|
||||
if (uri == null || !uri.toFile().exists()) {
|
||||
@@ -222,12 +223,12 @@ actual class VideoPlayer actual constructor(
|
||||
|
||||
return@withContext VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L)
|
||||
}
|
||||
val surface = SkiaBitmapVideoSurface()
|
||||
player.videoSurface().set(surface)
|
||||
player.media().startPaused(uri.toFile().absolutePath)
|
||||
val start = System.currentTimeMillis()
|
||||
var snap: BufferedImage? = null
|
||||
while (snap == null && start + 1500 > System.currentTimeMillis()) {
|
||||
snap = player.snapshots()?.get()
|
||||
delay(50)
|
||||
val snap = withTimeoutOrNull(1500L) {
|
||||
while (surface.bitmap.value == null) delay(50)
|
||||
surface.bitmap.value!!.toAwtImage()
|
||||
}
|
||||
val orientation = player.media().info().videoTracks().firstOrNull()?.orientation()
|
||||
if (orientation == null) {
|
||||
@@ -255,6 +256,7 @@ actual class VideoPlayer actual constructor(
|
||||
}
|
||||
|
||||
val playerThread = Executors.newSingleThreadExecutor()
|
||||
private val previewThread = Executors.newSingleThreadExecutor()
|
||||
private val playersPool: ArrayList<Component> = ArrayList()
|
||||
private val helperPlayersPool: ArrayList<CallbackMediaPlayerComponent> = ArrayList()
|
||||
|
||||
|
||||
+38
-12
@@ -26,6 +26,8 @@ import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
import kotlin.math.min
|
||||
|
||||
data class SemVer(
|
||||
@@ -376,7 +378,7 @@ private fun chooseGitHubReleaseAssets(release: GitHubRelease): List<GitHubAsset>
|
||||
val res = if (isRunningFromFlatpak()) {
|
||||
// No need to show download options for Flatpak users
|
||||
emptyList()
|
||||
} else if (!isRunningFromAppImage() && Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) {
|
||||
} else if (desktopPlatform.isLinux() && !isRunningFromAppImage() && Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) {
|
||||
// Show all available .deb packages and user will choose the one that works on his system (for Debian derivatives)
|
||||
release.assets.filter { it.name.lowercase().endsWith(".deb") }
|
||||
} else {
|
||||
@@ -388,18 +390,42 @@ private fun chooseGitHubReleaseAssets(release: GitHubRelease): List<GitHubAsset>
|
||||
private suspend fun installAppUpdate(file: File) = withContext(Dispatchers.IO) {
|
||||
when {
|
||||
desktopPlatform.isLinux() -> {
|
||||
val process = Runtime.getRuntime().exec("xdg-open ${file.absolutePath}").onExit().join()
|
||||
val startedInstallation = process.exitValue() == 0 && process.children().count() > 0
|
||||
if (!startedInstallation) {
|
||||
Log.e(TAG, "Error starting installation: ${process.inputReader().use { it.readLines().joinToString("\n") }}${process.errorStream.use { String(it.readAllBytes()) }}")
|
||||
// Failed to start installation. show directory with the file for manual installation
|
||||
desktopOpenDir(file.parentFile)
|
||||
val appImagePath = System.getenv("APPIMAGE")
|
||||
if (appImagePath != null) {
|
||||
// Replace the running AppImage crash-safely: copy onto the target's own
|
||||
// filesystem first (an atomic rename only works within one filesystem, and
|
||||
// the download lives in the temp dir which is usually a different one),
|
||||
// then atomically move the staged file onto $APPIMAGE.
|
||||
val target = File(appImagePath)
|
||||
val staging = File(target.parentFile, ".${target.name}.update")
|
||||
try {
|
||||
Files.copy(file.toPath(), staging.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
staging.setExecutable(true, false)
|
||||
Files.move(staging.toPath(), target.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING)
|
||||
file.delete()
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title),
|
||||
text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to replace AppImage: ${e.stackTraceToString()}")
|
||||
staging.delete()
|
||||
desktopOpenDir(file.parentFile)
|
||||
}
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title),
|
||||
text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc)
|
||||
)
|
||||
file.delete()
|
||||
val process = Runtime.getRuntime().exec("xdg-open ${file.absolutePath}").onExit().join()
|
||||
val startedInstallation = process.exitValue() == 0 && process.children().count() > 0
|
||||
if (!startedInstallation) {
|
||||
Log.e(TAG, "Error starting installation: ${process.inputReader().use { it.readLines().joinToString("\n") }}${process.errorStream.use { String(it.readAllBytes()) }}")
|
||||
// Failed to start installation. show directory with the file for manual installation
|
||||
desktopOpenDir(file.parentFile)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title),
|
||||
text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc)
|
||||
)
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
desktopPlatform.isWindows() -> {
|
||||
|
||||
@@ -24,13 +24,13 @@ android.nonTransitiveRClass=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=6.5.2
|
||||
android.version_code=349
|
||||
android.version_name=6.5.3
|
||||
android.version_code=351
|
||||
|
||||
android.bundle=false
|
||||
|
||||
desktop.version_name=6.5.2
|
||||
desktop.version_code=143
|
||||
desktop.version_name=6.5.3
|
||||
desktop.version_code=144
|
||||
|
||||
kotlin.version=2.1.20
|
||||
gradle.plugin.version=8.7.0
|
||||
|
||||
@@ -970,6 +970,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
GLPConnectingProhibit _ -> sendMessage cc ct $ "Already connecting to this " <> gt <> "."
|
||||
GLPConnectingConfirmReconnect -> sendMessage cc ct $ "Already connecting to this " <> gt <> "."
|
||||
GLPNoRelays _ -> sendMessage cc ct $ T.toTitle gt <> " has no active relays. Please try again later."
|
||||
GLPUpdateRequired _ -> sendMessage cc ct $ T.toTitle gt <> " requires a newer version."
|
||||
GLPOwnLink _ -> sendMessage cc ct "Unexpected error. Please report it to directory admins."
|
||||
_ -> sendMessage cc ct "Unexpected error. Please report it to directory admins."
|
||||
|
||||
|
||||
@@ -165,6 +165,9 @@ This file is generated automatically.
|
||||
- [SecurityCode](#securitycode)
|
||||
- [SimplePreference](#simplepreference)
|
||||
- [SimplexLinkType](#simplexlinktype)
|
||||
- [SimplexNameInfo](#simplexnameinfo)
|
||||
- [SimplexNameType](#simplexnametype)
|
||||
- [SimplexTLD](#simplextld)
|
||||
- [SndCIStatusProgress](#sndcistatusprogress)
|
||||
- [SndConnEvent](#sndconnevent)
|
||||
- [SndError](#snderror)
|
||||
@@ -2088,6 +2091,10 @@ SimplexLink:
|
||||
- simplexUri: string
|
||||
- smpHosts: [string]
|
||||
|
||||
SimplexName:
|
||||
- type: "simplexName"
|
||||
- nameInfo: [SimplexNameInfo](#simplexnameinfo)
|
||||
|
||||
Command:
|
||||
- type: "command"
|
||||
- commandStr: string
|
||||
@@ -2328,6 +2335,10 @@ NoRelays:
|
||||
- type: "noRelays"
|
||||
- groupSLinkData_: [GroupShortLinkData](#groupshortlinkdata)?
|
||||
|
||||
UpdateRequired:
|
||||
- type: "updateRequired"
|
||||
- groupSLinkData_: [GroupShortLinkData](#groupshortlinkdata)?
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -3434,6 +3445,36 @@ A_QUEUE:
|
||||
- "relay"
|
||||
|
||||
|
||||
---
|
||||
|
||||
## SimplexNameInfo
|
||||
|
||||
**Record type**:
|
||||
- nameType: [SimplexNameType](#simplexnametype)
|
||||
- nameTLD: [SimplexTLD](#simplextld)
|
||||
- domain: string
|
||||
- subDomain: [string]
|
||||
|
||||
|
||||
---
|
||||
|
||||
## SimplexNameType
|
||||
|
||||
**Enum type**:
|
||||
- "publicGroup"
|
||||
- "contact"
|
||||
|
||||
|
||||
---
|
||||
|
||||
## SimplexTLD
|
||||
|
||||
**Enum type**:
|
||||
- "simplex"
|
||||
- "testing"
|
||||
- "web"
|
||||
|
||||
|
||||
---
|
||||
|
||||
## SndCIStatusProgress
|
||||
|
||||
@@ -345,6 +345,9 @@ chatTypesDocsData =
|
||||
(sti @SecurityCode, STRecord, "", [], "", ""),
|
||||
(sti @SimplePreference, STRecord, "", [], "", ""),
|
||||
(sti @SimplexLinkType, STEnum, "XL", [], "", ""),
|
||||
(sti @SimplexNameInfo, STRecord, "", [], "", ""),
|
||||
(sti @SimplexNameType, STEnum, "NT", [], "", ""),
|
||||
(sti @SimplexTLD, STEnum, "TLD", [], "", ""),
|
||||
(sti @SMPAgentError, STUnion, "", [], "", ""),
|
||||
(sti @SndCIStatusProgress, STEnum, "SSP", [], "", ""),
|
||||
(sti @SndConnEvent, STUnion, "SCE", [], "", ""),
|
||||
@@ -558,6 +561,9 @@ deriving instance Generic RelayStatus
|
||||
deriving instance Generic ReportReason
|
||||
deriving instance Generic SecurityCode
|
||||
deriving instance Generic SimplexLinkType
|
||||
deriving instance Generic SimplexNameInfo
|
||||
deriving instance Generic SimplexNameType
|
||||
deriving instance Generic SimplexTLD
|
||||
deriving instance Generic SMPAgentError
|
||||
deriving instance Generic SndCIStatusProgress
|
||||
deriving instance Generic SndConnEvent
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: f0b7a4be7325cb787297a881076299c5ffbe26e7
|
||||
tag: 04960864c4ec958c21c20e2a7d618524138b2e72
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
# Public Namespaces for SimpleX Network
|
||||
|
||||
## Motivation
|
||||
|
||||
SimpleX has no user identifiers - users exchange invitation links out-of-band to connect. Short links help but are unmemorable. Public namespaces map human-readable names to SimpleX addresses.
|
||||
|
||||
Names also solve censorship at two levels. A short link is controlled by one SMP router - that router can delete it. An on-chain name can't be deleted by any router. If the link is removed, the owner points the name to a new link on a different router. At the network level, links can be URL-filtered, but names resolve through SMP proxy chains - censoring a name requires controlling all resolvers the user can reach.
|
||||
|
||||
DNS-based naming is vulnerable to domain seizure and requires WHOIS entries. Blockchains provide censorship-resistant globally unique names.
|
||||
|
||||
## Product requirements
|
||||
|
||||
### MVP
|
||||
|
||||
- **Names**: TLD `.simplex` (e.g., `privacy.simplex`, `my-channel.simplex`). Subdomains: `support.acme.simplex`. In markdown, `.simplex` can be omitted: `#privacy` = `privacy.simplex`.
|
||||
- **Name rules**: see [Name rules](#name-rules).
|
||||
- **Two address types**: each name stores channel links (set) and contact links (set). Client uses the first; set provides forward-compatible redundancy. Either can be empty.
|
||||
- **Optional metadata**: admin SimpleX address, admin email.
|
||||
- **Registration**: commit-reveal to prevent frontrunning. Length-based ETH pricing. Annual renewal. Dutch auction on expiry.
|
||||
- **Launch gating**: requires SimpleX test NFT. Up to 5 paid + 5 test names per holder. Test names free, auto-removed after 3 months, use `testing` namespace.
|
||||
- **Reserved names**: common verticals (books, games, music, movies, news, etc.) reserved for community-operated channels managed by SimpleX Network Consortium.
|
||||
- Only 7+ character names can be registered during "launch phase".
|
||||
- **Resolution**: client queries two independent name servers (Ethereum light clients) via two SMP proxies. Agreement = trusted. Disagreement = warning.
|
||||
- **Double resolution**: name -> short link (on-chain), short link -> connection data (existing protocol).
|
||||
- **Verification**: if on-chain link matches profile address, name is verified. Manual "verify" button + optional auto-verify on profile open.
|
||||
- **Markdown**: `#name` (`.simplex` implied), `#name.simplex` (explicit), `#name.testing` for test namespace. In CLI, `#` is local in group commands, global in `/c` and message bodies.
|
||||
- **Search**: `#name.simplex` auto-resolves. Disable in "More privacy" settings.
|
||||
- **Router role**: `names` added to `ServerRoles`. Not all routers support it.
|
||||
- **Contract**: ENS fork on Ethereum mainnet. ETH payment. Upgradeable.
|
||||
|
||||
### Post-MVP
|
||||
|
||||
- **Multiple links**: redundant entries per name. Forward-compatible schema in MVP where practical.
|
||||
- **Contact syntax**: `:name.simplex`, `:my-name.simplex`. Same namespace, different link type. MVP parser supports this syntax; resolution works; UI support is post-MVP.
|
||||
- **Community Credits**: replace ETH for private registration.
|
||||
- **Unicode expansion**: add scripts as user base grows.
|
||||
|
||||
## Part 1: Blockchain contract
|
||||
|
||||
### Overview
|
||||
|
||||
ENS fork on Ethereum mainnet. Retains commit-reveal, pricing, expiry, Dutch auction. Compatible with ENS dApp. Upgradeable.
|
||||
|
||||
ENS source:
|
||||
- Contracts: https://github.com/ensdomains/ens-contracts
|
||||
- dApp: https://github.com/ensdomains/ens-app-v3
|
||||
- JS library: https://github.com/ensdomains/ensjs
|
||||
|
||||
### Contract state
|
||||
|
||||
```
|
||||
Name record (ENS structure + SimpleX resolver fields):
|
||||
owner : address
|
||||
channelLinks : string[]
|
||||
contactLinks : string[]
|
||||
adminAddress : string -- optional
|
||||
adminEmail : string -- optional
|
||||
expiry : uint256
|
||||
isTest : bool
|
||||
|
||||
Global state:
|
||||
reservedNames : mapping(string => bool)
|
||||
testNFT : address
|
||||
registrationLimit : uint8 -- 5
|
||||
testLimit : uint8 -- 5
|
||||
```
|
||||
|
||||
There must be maps to track names by owner, but specific contract design should be based on ENS.
|
||||
|
||||
### Name rules
|
||||
|
||||
ENS normalization (ENSIP-15) with additional restrictions enforced in dApp (registration) and resolvers (resolution). Contract follows ENS as-is.
|
||||
|
||||
Additional restrictions beyond ENSIP-15:
|
||||
- No consecutive hyphens.
|
||||
- No accented characters. Latin is `a-z` only (same as DNS LDH rule).
|
||||
- Allowed scripts: Latin, Cyrillic, Arabic, Hebrew, Devanagari, Bengali, Thai, Greek, CJK, Hangul, Kana. Expandable as user base grows.
|
||||
|
||||
### Registration flow
|
||||
|
||||
1. NFT check
|
||||
2. Limit check (5 paid / 5 test)
|
||||
3. `commit(hash(name, owner, secret))`
|
||||
4. Wait (min 1 minute)
|
||||
5. `reveal(name, owner, secret)` + ETH (zero for test)
|
||||
6. Validate: well-formed, not taken, not reserved, fee covered
|
||||
7. Store record
|
||||
|
||||
### Pricing
|
||||
|
||||
Annual fees by name length:
|
||||
|
||||
| Length | Fee |
|
||||
|---|---|
|
||||
| 7+ | base |
|
||||
| 6 | 4x |
|
||||
| 5 | 16x |
|
||||
| 4 | 64x |
|
||||
| 3 | 256x |
|
||||
|
||||
Test names: free, expire after 3 months.
|
||||
|
||||
### Renewal and expiry
|
||||
|
||||
Annual renewal. Grace period, then Dutch auction decaying to base price.
|
||||
|
||||
### Updates
|
||||
|
||||
Owner can update links, admin address, admin email. Transfer follows ENS mechanics.
|
||||
|
||||
### Reserved names
|
||||
|
||||
List for community channels (e.g., `books`, `games`, `music`, `news`):
|
||||
- Not registrable by users
|
||||
- Revenue shared with network
|
||||
|
||||
### Retained ENS features
|
||||
|
||||
- **Resolver pattern**: registry maps name -> (owner, resolver). A SimpleX Resolver contract stores channel links, contact links, admin fields. Allows future extensibility without registry changes.
|
||||
- **Multicoin address records**: BTC/ETH/XMR donation addresses per name. Subscribers see donation options from name resolution.
|
||||
- **Text records**: generic key-value store for future metadata without contract upgrades.
|
||||
- **Reverse resolution**: name lookup by address. Enables verification and discovery.
|
||||
- **Subdomain registrar**: owner of `acme.simplex` can create `support.acme.simplex`, `sales.acme.simplex` without additional on-chain registration.
|
||||
|
||||
### Removed ENS features
|
||||
|
||||
- Avatar/image records.
|
||||
- `.eth` TLD and ENS name imports.
|
||||
- DNS name registration (DNSSEC imports).
|
||||
|
||||
### Governance
|
||||
|
||||
SimpleX Chat during testing and launch phases, migration to SimpleX Network Consortium.
|
||||
|
||||
## Part 2: SMP protocol extension
|
||||
|
||||
### New router role
|
||||
|
||||
```haskell
|
||||
data ServerRoles = ServerRoles
|
||||
{ storage :: Bool,
|
||||
proxy :: Bool,
|
||||
names :: Bool
|
||||
}
|
||||
```
|
||||
|
||||
Name-capable routers run an Ethereum light client.
|
||||
|
||||
### Resolution protocol
|
||||
|
||||
Uses existing SMP proxy infrastructure. Client sends queries through a proxy, not directly to name servers.
|
||||
|
||||
#### Commands
|
||||
|
||||
```
|
||||
Client -> Proxy -> Name Server:
|
||||
RSLV <namehash>
|
||||
|
||||
Name Server -> Proxy -> Client:
|
||||
NAME <name_record>
|
||||
ERR AUTH
|
||||
```
|
||||
|
||||
Forwarded via `PRXY`/`PFWD`/`RRES` mechanism.
|
||||
|
||||
#### Two-operator resolution
|
||||
|
||||
```
|
||||
Client -> Proxy A (Op 1) -> Name Server X (Op 1)
|
||||
Client -> Proxy B (Op 2) -> Name Server Y (Op 2)
|
||||
```
|
||||
|
||||
Both read same Ethereum state.
|
||||
|
||||
- Agree: trusted
|
||||
- Disagree: warn, don't use
|
||||
- One fails: retry with another server or show single result with reduced trust
|
||||
|
||||
Proxy sees client IP and session, but not query. Name server sees query, not client IP or session.
|
||||
|
||||
#### Name server implementation
|
||||
|
||||
1. Runs Ethereum light client (e.g., Helios) tracking SNRC
|
||||
2. Receives `RSLV` via SMP proxy
|
||||
3. Returns record from local state
|
||||
|
||||
State proofs can be added post-MVP.
|
||||
|
||||
#### Configuration
|
||||
|
||||
```haskell
|
||||
data NamesConfig = NamesConfig
|
||||
{ ethereumEndpoint :: String,
|
||||
snrcAddress :: EthAddress,
|
||||
cacheSeconds :: Int
|
||||
}
|
||||
```
|
||||
|
||||
#### Versioning
|
||||
|
||||
New SMP protocol version. Older routers/clients don't advertise the capability.
|
||||
|
||||
### Default routers
|
||||
|
||||
Default router list updated to include name-capable routers.
|
||||
|
||||
## Part 3: UI integration
|
||||
|
||||
### Markdown
|
||||
|
||||
- `#name` or `#name.simplex` - native names (no dot = `.simplex` implied)
|
||||
- `#my-name` or `#my-name.simplex` - hyphenated names
|
||||
- `#sub.name.simplex` - subdomains (explicit TLD)
|
||||
- `#name.testing` - test namespace
|
||||
- Rendered as clickable resolve-and-connect links
|
||||
|
||||
CLI: `#` = local in group commands, global in `/c` and messages.
|
||||
|
||||
`:name.simplex`, `:my-name.simplex` - contact addresses (same namespace, different link type). MVP parser supports this syntax; resolution works; UI support is post-MVP.
|
||||
|
||||
### Resolution flow
|
||||
|
||||
1. Normalize per ENSIP-15, compute namehash
|
||||
2. `RSLV` to two name servers via two proxies
|
||||
3. Compare results
|
||||
4. First channel link -> short link resolution -> connection data
|
||||
5. Present for joining
|
||||
|
||||
### Search
|
||||
|
||||
`#...simplex` triggers resolution. Disable in "More privacy" settings.
|
||||
|
||||
### Verification
|
||||
|
||||
On-chain link matches profile address = verified. Only name owner can set on-chain links.
|
||||
|
||||
- Manual: "Verify" button resolves and compares
|
||||
- Auto: optional setting, resolves on profile open
|
||||
|
||||
### Display
|
||||
|
||||
Show name and verification status. `#` is syntax, not part of the name.
|
||||
|
||||
## Open questions
|
||||
|
||||
1. **Contract upgrade mechanism**: proxy pattern with timelock? Migration path for future Community Credits payment and domain name support.
|
||||
@@ -2352,6 +2352,7 @@ export type Format =
|
||||
| Format.Uri
|
||||
| Format.HyperLink
|
||||
| Format.SimplexLink
|
||||
| Format.SimplexName
|
||||
| Format.Command
|
||||
| Format.Mention
|
||||
| Format.Email
|
||||
@@ -2369,6 +2370,7 @@ export namespace Format {
|
||||
| "uri"
|
||||
| "hyperLink"
|
||||
| "simplexLink"
|
||||
| "simplexName"
|
||||
| "command"
|
||||
| "mention"
|
||||
| "email"
|
||||
@@ -2425,6 +2427,11 @@ export namespace Format {
|
||||
smpHosts: string[] // non-empty
|
||||
}
|
||||
|
||||
export interface SimplexName extends Interface {
|
||||
type: "simplexName"
|
||||
nameInfo: SimplexNameInfo
|
||||
}
|
||||
|
||||
export interface Command extends Interface {
|
||||
type: "command"
|
||||
commandStr: string
|
||||
@@ -2596,6 +2603,7 @@ export type GroupLinkPlan =
|
||||
| GroupLinkPlan.ConnectingProhibit
|
||||
| GroupLinkPlan.Known
|
||||
| GroupLinkPlan.NoRelays
|
||||
| GroupLinkPlan.UpdateRequired
|
||||
|
||||
export namespace GroupLinkPlan {
|
||||
export type Tag =
|
||||
@@ -2605,6 +2613,7 @@ export namespace GroupLinkPlan {
|
||||
| "connectingProhibit"
|
||||
| "known"
|
||||
| "noRelays"
|
||||
| "updateRequired"
|
||||
|
||||
interface Interface {
|
||||
type: Tag
|
||||
@@ -2643,6 +2652,11 @@ export namespace GroupLinkPlan {
|
||||
type: "noRelays"
|
||||
groupSLinkData_?: GroupShortLinkData
|
||||
}
|
||||
|
||||
export interface UpdateRequired extends Interface {
|
||||
type: "updateRequired"
|
||||
groupSLinkData_?: GroupShortLinkData
|
||||
}
|
||||
}
|
||||
|
||||
export interface GroupMember {
|
||||
@@ -3836,6 +3850,24 @@ export enum SimplexLinkType {
|
||||
Relay = "relay",
|
||||
}
|
||||
|
||||
export interface SimplexNameInfo {
|
||||
nameType: SimplexNameType
|
||||
nameTLD: SimplexTLD
|
||||
domain: string
|
||||
subDomain: string[]
|
||||
}
|
||||
|
||||
export enum SimplexNameType {
|
||||
PublicGroup = "publicGroup",
|
||||
Contact = "contact",
|
||||
}
|
||||
|
||||
export enum SimplexTLD {
|
||||
Simplex = "simplex",
|
||||
Testing = "testing",
|
||||
Web = "web",
|
||||
}
|
||||
|
||||
export enum SndCIStatusProgress {
|
||||
Partial = "partial",
|
||||
Complete = "complete",
|
||||
|
||||
@@ -1683,6 +1683,10 @@ class Format_simplexLink(TypedDict):
|
||||
simplexUri: str
|
||||
smpHosts: list[str] # non-empty
|
||||
|
||||
class Format_simplexName(TypedDict):
|
||||
type: Literal["simplexName"]
|
||||
nameInfo: "SimplexNameInfo"
|
||||
|
||||
class Format_command(TypedDict):
|
||||
type: Literal["command"]
|
||||
commandStr: str
|
||||
@@ -1708,13 +1712,14 @@ Format = (
|
||||
| Format_uri
|
||||
| Format_hyperLink
|
||||
| Format_simplexLink
|
||||
| Format_simplexName
|
||||
| Format_command
|
||||
| Format_mention
|
||||
| Format_email
|
||||
| Format_phone
|
||||
)
|
||||
|
||||
Format_Tag = Literal["bold", "italic", "strikeThrough", "snippet", "secret", "small", "colored", "uri", "hyperLink", "simplexLink", "command", "mention", "email", "phone"]
|
||||
Format_Tag = Literal["bold", "italic", "strikeThrough", "snippet", "secret", "small", "colored", "uri", "hyperLink", "simplexLink", "simplexName", "command", "mention", "email", "phone"]
|
||||
|
||||
class FormattedText(TypedDict):
|
||||
format: NotRequired["Format"]
|
||||
@@ -1850,6 +1855,10 @@ class GroupLinkPlan_noRelays(TypedDict):
|
||||
type: Literal["noRelays"]
|
||||
groupSLinkData_: NotRequired["GroupShortLinkData"]
|
||||
|
||||
class GroupLinkPlan_updateRequired(TypedDict):
|
||||
type: Literal["updateRequired"]
|
||||
groupSLinkData_: NotRequired["GroupShortLinkData"]
|
||||
|
||||
GroupLinkPlan = (
|
||||
GroupLinkPlan_ok
|
||||
| GroupLinkPlan_ownLink
|
||||
@@ -1857,9 +1866,10 @@ GroupLinkPlan = (
|
||||
| GroupLinkPlan_connectingProhibit
|
||||
| GroupLinkPlan_known
|
||||
| GroupLinkPlan_noRelays
|
||||
| GroupLinkPlan_updateRequired
|
||||
)
|
||||
|
||||
GroupLinkPlan_Tag = Literal["ok", "ownLink", "connectingConfirmReconnect", "connectingProhibit", "known", "noRelays"]
|
||||
GroupLinkPlan_Tag = Literal["ok", "ownLink", "connectingConfirmReconnect", "connectingProhibit", "known", "noRelays", "updateRequired"]
|
||||
|
||||
class GroupMember(TypedDict):
|
||||
groupMemberId: int # int64
|
||||
@@ -2679,6 +2689,16 @@ class SimplePreference(TypedDict):
|
||||
|
||||
SimplexLinkType = Literal["contact", "invitation", "group", "channel", "relay"]
|
||||
|
||||
class SimplexNameInfo(TypedDict):
|
||||
nameType: "SimplexNameType"
|
||||
nameTLD: "SimplexTLD"
|
||||
domain: str
|
||||
subDomain: list[str]
|
||||
|
||||
SimplexNameType = Literal["publicGroup", "contact"]
|
||||
|
||||
SimplexTLD = Literal["simplex", "testing", "web"]
|
||||
|
||||
SndCIStatusProgress = Literal["partial", "complete"]
|
||||
|
||||
class SndConnEvent_switchQueue(TypedDict):
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Desktop: video playback hangs after a preview snapshot stalls
|
||||
|
||||
Branch: `nd/fix-video` · final code commit `4c7073bdc` · PR [#6983](https://github.com/simplex-chat/simplex-chat/pull/6983).
|
||||
|
||||
## 1. Problem statement
|
||||
|
||||
On Desktop with several videos in a chat, clicking the play button on the second (or any subsequent) video does nothing. The first video plays normally; later ones present a play button that responds to the click but never starts playback. No error dialog appears in the UI. `stderr` shows libvlc and libavcodec noise:
|
||||
|
||||
```
|
||||
[h264 @ 0x...] get_buffer() failed
|
||||
[h264 @ 0x...] thread_get_buffer() failed
|
||||
[h264 @ 0x...] decode_slice_header error
|
||||
[h264 @ 0x...] no frame!
|
||||
... main video output error: Failed to grab a snapshot
|
||||
```
|
||||
|
||||
The bug appeared after PR [#6924](https://github.com/simplex-chat/simplex-chat/pull/6924) (`ab2d03630`), which switched the preview helper player from the shared `vlcFactory` to a dedicated `vlcPreviewFactory` with `--avcodec-hw=none`. Hardware-accelerated decoding had previously masked the underlying fragility. Scope: Desktop only.
|
||||
|
||||
## 2. Root cause
|
||||
|
||||
Two compounding defects in `VideoPlayer.desktop.kt`, surfaced by `#6924`:
|
||||
|
||||
### 2a. Synchronous `snapshots().get()` blocks the shared `playerThread` indefinitely
|
||||
|
||||
`getBitmapFromVideo` ran inside `withContext(playerThread.asCoroutineDispatcher())` — the same single-thread executor used by `play()`/`stop()` for playback. Its loop polls vlcj's snapshot API:
|
||||
|
||||
```kotlin
|
||||
while (snap == null && start + 1500 > System.currentTimeMillis()) {
|
||||
snap = player.snapshots()?.get()
|
||||
delay(50)
|
||||
}
|
||||
```
|
||||
|
||||
The 1500 ms wall-clock guard only fires *between* calls. `player.snapshots()?.get()` is a synchronous JNI call that, when libvlc cannot produce a frame, waits indefinitely. While it blocks, `playerThread` is held: every queued `playerThread.execute { videoPlaying.value = start(...) }` from a subsequent `play()` click sits in the queue and never runs.
|
||||
|
||||
This was confirmed by instrumented printlns: after the first video's preview entered the snapshot loop, the second video's `play()` body executed (UI thread println fires), but its lambda submitted to `playerThread.execute` produced no `lambda started` print — because `playerThread` was stuck inside the JNI call.
|
||||
|
||||
### 2b. Helper-player pool reuse exhausts the software h264 buffer pool
|
||||
|
||||
`getOrCreateHelperPlayer()` returns a `CallbackMediaPlayerComponent` from `helperPlayersPool`, recycling it across preview generations. With `vlcFactory` (hardware-accelerated by default), this was harmless — the GPU buffer pool was large with different lifecycle semantics. After `#6924` switched the helper to `vlcPreviewFactory` (`--avcodec-hw=none`), libavcodec frames from the previous run were not released cleanly across `stop` + `startPaused`, and the second decoder ran out of buffers (`get_buffer() failed`). The vout never produced a frame, which is the trigger for the hang in 2a.
|
||||
|
||||
## 3. Solution summary
|
||||
|
||||
`apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt` — single file, +8 / −6 lines. Helper-player pool is preserved as-is.
|
||||
|
||||
1. **Replace the polling `snapshots().get()` loop with a `CallbackVideoSurface` capture wrapped in `withTimeoutOrNull`.** The existing `SkiaBitmapVideoSurface` (already used for full-screen playback rendering) is attached to the helper player before `media().startPaused(...)`. Its `RenderCallback.display()` runs as soon as libvlc decodes the first frame, populating `surface.bitmap`. `getBitmapFromVideo` polls `surface.bitmap.value` from inside `withTimeoutOrNull(1500L) { ... }`; the wait is now structurally bounded — the synchronous JNI call is gone. Frame is converted to `BufferedImage` via `ImageBitmap.toAwtImage()` for the existing orientation-correction code path. This addresses 2a directly: a helper that fails to decode (2b) no longer holds the dispatcher.
|
||||
|
||||
2. **Move preview generation to a dedicated executor.** A new `previewThread = Executors.newSingleThreadExecutor()` runs `getBitmapFromVideo`. Defense in depth: even if 1500 ms of preview work overlaps with a play click, playback's `playerThread` is free to service it.
|
||||
|
||||
The pool is intentionally not touched. Removing it loses the factory-warmup amortization across distinct video URIs without addressing the actual hang (which is in the synchronous snapshot API, not in player reuse).
|
||||
|
||||
## 4. Alternatives considered (and rejected)
|
||||
|
||||
- **Drop the helper-player pool (initial attempt, commit `4a964c661`).** Replaces every preview's helper with a fresh `CallbackMediaPlayerComponent`. Fixes the symptom by sidestepping pool reuse, but costs the factory-warmup benefit and does not address the underlying blocking JNI call — a single corrupt video could still hang preview generation indefinitely (just on a fresh helper). Superseded by the surface-capture approach.
|
||||
- **Keep the pool, reset the helper between uses.** vlcj has no clean reset API; would require `media().release()` + manual re-attach. More code, fragile, doesn't address 2a.
|
||||
- **Wrap `snapshots().get()` in a coroutine timeout on a separate IO thread.** `withTimeoutOrNull` cannot cancel a blocked JNI call; the IO thread leaks until libvlc returns (which may be never).
|
||||
- **Revert PR #6924.** Restores the masking effect of hardware-accelerated decoding but reintroduces whatever the PR was guarding against, and leaves both 2a and 2b in place.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Desktop In-App Updater Fixes
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The desktop in-app updater (`apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt`) silently or visibly fails on three of the four supported desktop platforms:
|
||||
|
||||
1. **Windows**: no update dialog ever appears for any Windows user, regardless of how out-of-date the running version is.
|
||||
2. **AppImage (x86_64)**: the update dialog appears and the download succeeds, but clicking "Install update" opens the new AppImage in an archive viewer ("Archive format not recognized") instead of installing it. The running AppImage is never replaced.
|
||||
3. **AppImage (aarch64)**: no update dialog ever appears for any aarch64 AppImage user.
|
||||
|
||||
The desktop installer flow on macOS and the `.deb` flow on Debian-derivative Linux are not affected and remain unchanged.
|
||||
|
||||
## Root Causes
|
||||
|
||||
### 1. Windows — `which dpkg` IOException swallowed
|
||||
|
||||
`chooseGitHubReleaseAssets` (AppUpdater.kt) invokes `Runtime.getRuntime().exec("which dpkg")` unconditionally to detect Debian-derivative systems. On Windows there is no `which.exe`; `CreateProcess` returns error 2 and the JVM throws `IOException: Cannot run program "which"` synchronously from `Runtime.exec`. The exception propagates up through `chooseGitHubReleaseAssets` into `checkForUpdate`'s outer `try { ... } catch (e: Exception) { Log.e(...) }`, which logs to stderr and returns. The user-facing alert is never built.
|
||||
|
||||
The `.deb` probe was correct in intent but executed too eagerly: it has no business running on a non-Linux platform.
|
||||
|
||||
### 2. AppImage — `xdg-open` is the wrong operation
|
||||
|
||||
The Linux branch of `installAppUpdate` calls `xdg-open <downloaded.AppImage>`. An AppImage is not "installable" in the package-manager sense — it is a self-contained executable that lives at the path stored in the `$APPIMAGE` environment variable (set by the AppImage runtime). On most desktop environments, `xdg-open` resolves the `.AppImage` MIME type to an archive handler (file-roller, ark, engrampa). The handler attempts to read the AppImage as a `.iso`/squashfs archive and fails with "Archive format not recognized". Even when it succeeds, it does not replace the running AppImage — the next launch still runs the old binary.
|
||||
|
||||
The existing code had no awareness of `$APPIMAGE` at install time. The `GitHubAsset.isAppImage` field hints at an earlier abandoned attempt at AppImage-specific handling.
|
||||
|
||||
### 3. aarch64 AppImage — leading space in asset name
|
||||
|
||||
`Platform.desktop.kt` declares:
|
||||
|
||||
```kotlin
|
||||
LINUX_AARCH64("so", unixConfigPath, unixDataPath, " simplex-desktop-aarch64.AppImage"),
|
||||
```
|
||||
|
||||
The `githubAssetName` literal has a leading space character. The actual release asset published by `.github/workflows/build.yml` is `simplex-desktop-aarch64.AppImage` (no space — verified against the live GitHub releases API). The exact-name filter in `chooseGitHubReleaseAssets` (`release.assets.filter { it.name == desktopPlatform.githubAssetName }`) never matches, the asset list is empty, and `checkForUpdate` returns at the "No assets to download for current system" branch without ever showing a dialog. Same silent-failure pattern as the Windows bug, single arch in blast radius.
|
||||
|
||||
## Solution Summary
|
||||
|
||||
Three small, independent commits — one per root cause. None of them changes shared logic; each touches one line (Windows, aarch64) or one branch of the install dispatch (AppImage).
|
||||
|
||||
### Fix 1 — Gate the `dpkg` probe on Linux
|
||||
|
||||
```kotlin
|
||||
// AppUpdater.kt: chooseGitHubReleaseAssets
|
||||
} else if (desktopPlatform.isLinux() && !isRunningFromAppImage()
|
||||
&& Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) {
|
||||
```
|
||||
|
||||
Single conjunct (`desktopPlatform.isLinux() &&`) added at the start of the `else if`. Boolean short-circuit ensures `Runtime.exec` is never reached on non-Linux. The added gate matches the actual semantic intent: `.deb` is a Linux-only package format. Both the Windows IOException and the (theoretical) macOS misbehavior of the `which` probe are eliminated.
|
||||
|
||||
### Fix 2 — AppImage-aware install path
|
||||
|
||||
In `installAppUpdate`'s Linux branch, read `System.getenv("APPIMAGE")`:
|
||||
|
||||
- If non-null, the running app is an AppImage at that path. Replacing it crash-safely takes two steps, because an atomic file replacement is only possible *within a single filesystem* (POSIX `rename(2)`), and the download lives in the temp dir — usually a different filesystem (tmpfs) from where `$APPIMAGE` lives:
|
||||
1. `Files.copy` the downloaded file to a staging file (`.<name>.update`) in the target's *own* directory. This is the unavoidable cross-filesystem transfer; it is not atomic, but it writes only a sidecar, never the live `$APPIMAGE`.
|
||||
2. Mark the staging file executable, then `Files.move` it onto `$APPIMAGE` with `ATOMIC_MOVE`. Because staging now shares the target's filesystem, this is a real atomic `rename(2)`: the live file flips from old to new in one indivisible step, never partially written.
|
||||
|
||||
A direct `Files.move(downloaded, target, REPLACE_EXISTING)` is **not** sufficient — across filesystems it copies bytes straight onto the live `$APPIMAGE`, which is neither atomic nor crash-safe (an interrupted copy destroys the user's installed app). `ATOMIC_MOVE` on a cross-filesystem move throws `AtomicMoveNotSupportedException`. Staging on the target's filesystem first is what makes the atomic move possible. On Linux the kernel keeps the running process's open file descriptors valid across the rename: the running app continues to function until the user restarts, at which point the new binary is used.
|
||||
- If null, fall back to the existing `xdg-open` path (used for `.deb` install on Debian, which is the only remaining caller of this path after Fix 2).
|
||||
|
||||
On any exception (permission denied if the AppImage lives in `/opt/`, target read-only, etc.) the catch deletes the staging file and falls back to `desktopOpenDir(file.parentFile)` — the same fallback the original `xdg-open` path used.
|
||||
|
||||
### Fix 3 — Remove leading space from `LINUX_AARCH64` asset name
|
||||
|
||||
```kotlin
|
||||
LINUX_AARCH64("so", unixConfigPath, unixDataPath, "simplex-desktop-aarch64.AppImage"),
|
||||
```
|
||||
|
||||
Single character removed. The asset name now matches what `make-appimage-linux.sh` produces and what GitHub releases publish.
|
||||
|
||||
## Why three commits, not one
|
||||
|
||||
Each fix has a different blast radius, a different fix size, and (potentially) a different review path. Three focused commits let a reviewer judge each one in isolation:
|
||||
|
||||
- Windows fix: 1 line, gates a side-effecting `Runtime.exec` on a platform check that the surrounding code already establishes.
|
||||
- AppImage install: ~35 lines, introduces new file-system operations (`Files.copy` to a staging file, then `Files.move` with `ATOMIC_MOVE`).
|
||||
- aarch64 fix: 1 character, fixes a typo in a string literal.
|
||||
|
||||
Bundling them as a single commit would force a reviewer to verify all three at once and would obscure `git blame` on the AppImage install logic, which is the only one of the three that introduces meaningful new behavior.
|
||||
|
||||
## Out of scope
|
||||
|
||||
The following were identified during the audit (`apps/multiplatform/app-updater-audit.md`) but deliberately deferred to keep this PR focused:
|
||||
|
||||
- `msiexec /i ${file.absolutePath}` uses the single-string `Runtime.exec` overload that tokenizes on whitespace; paths containing spaces (uncommon on Windows but possible) break the install.
|
||||
- Download failures (network, TLS, disk-full, GitHub error) are caught but only logged; the user sees nothing.
|
||||
- `process.children().count() > 0` in the Linux `xdg-open` path is racy and arguably wrong.
|
||||
- No SHA256 / signature verification on the downloaded artifact — the updater installs whatever GitHub serves.
|
||||
- 24h delay with no retry / backoff on transient network errors.
|
||||
- macOS install hardcodes `/Applications/SimpleX.app`.
|
||||
|
||||
Each is documented with `file:line` references in the audit; none affects the three platforms this PR fixes.
|
||||
|
||||
## Test plan
|
||||
|
||||
- **Windows**: built x86_64 MSI via the fork CI workflow [`build-windows-msi.yml`](https://github.com/Narasimha-sc/simplex-chat/actions/runs/25958413517), installed in a Windows VM as version 6.5.1 (intentionally lowered to trigger the check against current stable 6.5.2). Settings → Check for updates → Stable: dialog appeared as expected.
|
||||
- **AppImage x86_64**: built locally (host build, GHC 9.6.3, gradle createDistributable, appimagetool), installed and ran on Linux. Settings → Check for updates → Stable: dialog appeared, Download landed file at `/tmp/simplex/simplex-desktop-x86_64.AppImage`, Install replaced `$APPIMAGE` in place. Verified by hashing `$APPIMAGE` before and after.
|
||||
- **aarch64 AppImage**: not separately tested. Fix is a 1-character literal change verified against the live GitHub releases API (`simplex-desktop-aarch64.AppImage`, no leading space).
|
||||
- **macOS**: no changes to the macOS install branch.
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."f0b7a4be7325cb787297a881076299c5ffbe26e7" = "0a8a9l31l4a9nilcqg8h60mrxpqxpzzqxi58i60nw8h4vxqqlzcz";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."04960864c4ec958c21c20e2a7d618524138b2e72" = "0bxirnczwxjdm8jdvqalf0jvllvpmvbxwvjcz6aq9z7k7rx8ijai";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||
|
||||
@@ -1053,6 +1053,7 @@ data GroupLinkPlan
|
||||
| GLPConnectingProhibit {groupInfo_ :: Maybe GroupInfo}
|
||||
| GLPKnown {groupInfo :: GroupInfo, groupUpdated :: BoolDef, ownerVerification :: Maybe OwnerVerification, linkOwners :: ListDef GroupLinkOwner}
|
||||
| GLPNoRelays {groupSLinkData_ :: Maybe GroupShortLinkData}
|
||||
| GLPUpdateRequired {groupSLinkData_ :: Maybe GroupShortLinkData}
|
||||
deriving (Show)
|
||||
|
||||
data GroupLinkOwner = GroupLinkOwner
|
||||
@@ -1098,6 +1099,7 @@ connectionPlanProceed = \case
|
||||
GLPOwnLink _ -> True
|
||||
GLPConnectingConfirmReconnect -> True
|
||||
GLPNoRelays _ -> False
|
||||
GLPUpdateRequired _ -> False
|
||||
_ -> False
|
||||
CPError _ -> True
|
||||
|
||||
|
||||
@@ -4132,21 +4132,25 @@ processChatCommand vr nm = \case
|
||||
Nothing -> do
|
||||
(fd, cData@(ContactLinkData _ UserContactData {direct, owners, relays})) <- getShortLinkConnReq' nm user l'
|
||||
groupSLinkData_ <- liftIO $ decodeLinkUserData cData
|
||||
if not direct && null relays
|
||||
then pure (con (linkConnReq fd), CPGroupLink (GLPNoRelays groupSLinkData_))
|
||||
else do
|
||||
let FixedLinkData {linkConnReq = cReq, linkEntityId, rootKey} = fd
|
||||
linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, publicGroupId = B64UrlByteString <$> linkEntityId}
|
||||
let profilePGId = groupSLinkData_ >>= \GroupShortLinkData {groupProfile = GroupProfile {publicGroup}} ->
|
||||
fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId) publicGroup
|
||||
case (B64UrlByteString <$> linkEntityId, profilePGId) of
|
||||
(Just entityId, Just publicGroupId) | entityId == publicGroupId -> pure ()
|
||||
(Nothing, Nothing) -> pure ()
|
||||
_ -> throwChatError CEInvalidConnReq
|
||||
let ov = verifyLinkOwner rootKey owners l' sig_
|
||||
plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ ov
|
||||
pure (con cReq, plan)
|
||||
if
|
||||
| not direct && unsupportedGroupType groupSLinkData_ -> pure (con (linkConnReq fd), CPGroupLink (GLPUpdateRequired groupSLinkData_))
|
||||
| not direct && null relays -> pure (con (linkConnReq fd), CPGroupLink (GLPNoRelays groupSLinkData_))
|
||||
| otherwise -> do
|
||||
let FixedLinkData {linkConnReq = cReq, linkEntityId, rootKey} = fd
|
||||
linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, publicGroupId = B64UrlByteString <$> linkEntityId}
|
||||
let profilePGId = groupSLinkData_ >>= \GroupShortLinkData {groupProfile = GroupProfile {publicGroup}} ->
|
||||
fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId) publicGroup
|
||||
case (B64UrlByteString <$> linkEntityId, profilePGId) of
|
||||
(Just entityId, Just publicGroupId) | entityId == publicGroupId -> pure ()
|
||||
(Nothing, Nothing) -> pure ()
|
||||
_ -> throwChatError CEInvalidConnReq
|
||||
let ov = verifyLinkOwner rootKey owners l' sig_
|
||||
plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ ov
|
||||
pure (con cReq, plan)
|
||||
where
|
||||
unsupportedGroupType = \case
|
||||
Just GroupShortLinkData {groupProfile = GroupProfile {publicGroup = Just PublicGroupProfile {groupType}}} -> groupType /= GTChannel
|
||||
_ -> False
|
||||
knownLinkPlans = withFastStore $ \db ->
|
||||
liftIO (getGroupInfoViaUserShortLink db vr user l') >>= \case
|
||||
Just (cReq, g) -> pure $ Just (con cReq, CPGroupLink (GLPOwnLink g))
|
||||
@@ -5567,17 +5571,25 @@ mkValidName :: String -> String
|
||||
mkValidName = dropWhileEnd isSpace . take 50 . reverse . fst3 . foldl' addChar ("", '\NUL', 0 :: Int)
|
||||
where
|
||||
fst3 (x, _, _) = x
|
||||
addChar (r, prev, punct) c = if validChar then (c' : r, c', punct') else (r, prev, punct)
|
||||
addChar (r, prev, punct) c' = if validChar then (c : r, c, punct') else (r, prev, punct)
|
||||
where
|
||||
c' = if isSpace c then ' ' else c
|
||||
c = if isSpace c' then ' ' else c'
|
||||
cat = generalCategory c
|
||||
isPunct = case cat of
|
||||
ConnectorPunctuation -> True
|
||||
DashPunctuation -> True
|
||||
OtherPunctuation -> True
|
||||
_ -> False
|
||||
punct'
|
||||
| isPunctuation c = punct + 1
|
||||
| isSpace c = punct
|
||||
| isPunct = punct + 1
|
||||
| c == ' ' = punct
|
||||
| otherwise = 0
|
||||
validChar
|
||||
| c == '\'' = False
|
||||
| prev == '\NUL' = c > ' ' && c /= '#' && c /= '@' && validFirstChar
|
||||
| isSpace prev = validFirstChar || (punct == 0 && isPunctuation c)
|
||||
| isPunctuation prev = validFirstChar || isSpace c || (punct < 3 && isPunctuation c)
|
||||
| otherwise = validFirstChar || isSpace c || isMark c || isPunctuation c
|
||||
validFirstChar = isLetter c || isNumber c || isSymbol c
|
||||
| c `elem` prohibited = False
|
||||
| prev == '\NUL' = c > ' ' && validFirstNameChar
|
||||
| prev == ' ' = validFirstChar || (punct == 0 && isPunct)
|
||||
| punct > 0 = validFirstChar || c == ' '
|
||||
| otherwise = validFirstChar || c == ' ' || isMark c || isPunct
|
||||
validFirstNameChar = isLetter c || cat == DecimalNumber || cat == OtherSymbol
|
||||
validFirstChar = validFirstNameChar || cat == CurrencySymbol || cat == MathSymbol
|
||||
prohibited = ".,;/\\#@'\"`~" :: String
|
||||
|
||||
@@ -35,11 +35,11 @@ import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnReqUriData (..), ConnShortLink (..), ConnectionLink (..), ConnectionRequestUri (..), ContactConnType (..), SMPQueue (..), simplexConnReqUri, simplexShortLink)
|
||||
import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnReqUriData (..), ConnShortLink (..), ConnectionLink (..), ConnectionRequestUri (..), ContactConnType (..), SMPQueue (..), SimplexNameInfo (..), simplexConnReqUri, simplexShortLink)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (ProtocolServer (..))
|
||||
import Simplex.Messaging.Util (decodeJSON, safeDecodeUtf8, tshow)
|
||||
import Simplex.Messaging.Util (decodeJSON, safeDecodeUtf8, tshow, (<$?>))
|
||||
import System.Console.ANSI.Types
|
||||
import qualified Text.Email.Validate as Email
|
||||
import qualified URI.ByteString as U
|
||||
@@ -59,6 +59,7 @@ data Format
|
||||
-- showText is Nothing for the usual Uri without text
|
||||
| HyperLink {showText :: Maybe Text, linkUri :: Text}
|
||||
| SimplexLink {showText :: Maybe Text, linkType :: SimplexLinkType, simplexUri :: AConnectionLink, smpHosts :: NonEmpty Text}
|
||||
| SimplexName {nameInfo :: SimplexNameInfo}
|
||||
| Command {commandStr :: Text}
|
||||
| Mention {memberName :: Text}
|
||||
| Email
|
||||
@@ -184,6 +185,7 @@ isLink = \case
|
||||
Uri -> True
|
||||
HyperLink {} -> True
|
||||
SimplexLink {} -> True
|
||||
SimplexName {} -> True
|
||||
_ -> False
|
||||
|
||||
hasLinks :: MarkdownList -> Bool
|
||||
@@ -202,9 +204,9 @@ markdownP = mconcat <$> A.many' fragmentP
|
||||
'_' -> formattedP '_' Italic
|
||||
'~' -> formattedP '~' StrikeThrough
|
||||
'`' -> formattedP '`' Snippet
|
||||
'#' -> A.char '#' *> secretP
|
||||
'#' -> A.char '#' *> (secretP <|> nameRefP '#' <|> secretFallback)
|
||||
'!' -> styledP <|> wordP
|
||||
'@' -> mentionP <|> wordP
|
||||
'@' -> (A.char '@' *> nameRefP '@') <|> mentionP <|> wordP
|
||||
'/' -> commandP <|> wordP
|
||||
'[' -> sowLinkP <|> wordP
|
||||
_
|
||||
@@ -221,14 +223,29 @@ markdownP = mconcat <$> A.many' fragmentP
|
||||
unmarked $ c `T.cons` s `T.snoc` c
|
||||
| otherwise = markdown f s
|
||||
secretP :: Parser Markdown
|
||||
secretP = secret <$> A.takeWhile (== '#') <*> A.takeTill (== '#') <*> A.takeWhile (== '#')
|
||||
secret :: Text -> Text -> Text -> Markdown
|
||||
secret b s a
|
||||
| T.null a || T.null s || T.head s == ' ' || T.last s == ' ' =
|
||||
unmarked $ '#' `T.cons` ss
|
||||
| otherwise = markdown Secret $ T.init ss
|
||||
secretP = secret <$?> ((,,) <$> A.takeWhile (== '#') <*> A.takeTill (== '#') <*> A.takeWhile1 (== '#'))
|
||||
secret :: (Text, Text, Text) -> Either String Markdown
|
||||
secret (b, s, a)
|
||||
| T.null s || T.head s == ' ' || T.last s == ' ' = Left "not secret"
|
||||
| otherwise = Right $ markdown Secret $ T.init ss
|
||||
where
|
||||
ss = b <> s <> a
|
||||
secretFallback :: Parser Markdown
|
||||
secretFallback = unmarked . ('#' `T.cons`) <$> A.takeTill (== ' ')
|
||||
nameRefP :: Char -> Parser Markdown
|
||||
nameRefP pfx = nameRef <$?> A.takeTill (== ' ')
|
||||
where
|
||||
nameRef word
|
||||
| pfx == '@' && T.all (/= '.') name = Left "not a name"
|
||||
| otherwise = mkMd <$> strDecode (encodeUtf8 full)
|
||||
where
|
||||
(name, punct) = splitPunctuation word
|
||||
full = pfx `T.cons` name
|
||||
mkMd ni
|
||||
| T.null punct = md'
|
||||
| otherwise = md' :|: unmarked punct
|
||||
where
|
||||
md' = markdown (SimplexName ni) full
|
||||
styledP :: Parser Markdown
|
||||
styledP = do
|
||||
f <- A.char '!' *> ((A.char '-' $> Small) <|> (colored <$> colorP)) <* A.space
|
||||
@@ -449,6 +466,7 @@ markdownText (FormattedText f_ t) = case f_ of
|
||||
Uri -> t
|
||||
HyperLink {} -> t
|
||||
SimplexLink {} -> t
|
||||
SimplexName {} -> t
|
||||
Mention _ -> t
|
||||
Command _ -> t
|
||||
Email -> t
|
||||
@@ -479,7 +497,6 @@ displayNameTextP_ = (,"") <$> quoted '\'' <|> splitPunctuation <$> takeNameTill
|
||||
takeNameTill p =
|
||||
A.peekChar' >>= \c ->
|
||||
if refChar c then A.takeTill p else fail "invalid first character in display name"
|
||||
splitPunctuation s = (T.dropWhileEnd isPunctuation s, T.takeWhileEnd isPunctuation s)
|
||||
quoted c = A.char c *> takeNameTill (== c) <* A.char c
|
||||
refChar c = c > ' ' && c /= '#' && c /= '@' && c /= '\''
|
||||
|
||||
@@ -490,6 +507,9 @@ commandTextP = do
|
||||
(keyword : _) | T.all (\c -> isAlpha c || isDigit c || c == '_') keyword -> pure (cmd, punct)
|
||||
_ -> fail "invalid command keyword"
|
||||
|
||||
splitPunctuation :: Text -> (Text, Text)
|
||||
splitPunctuation s = (T.dropWhileEnd isPunctuation s, T.takeWhileEnd isPunctuation s)
|
||||
|
||||
-- quotes names that contain spaces or end on punctuation
|
||||
viewName :: Text -> Text
|
||||
viewName s = if T.any isSpace s || maybe False (isPunctuation . snd) (T.unsnoc s) then "'" <> s <> "'" else s
|
||||
|
||||
@@ -2150,6 +2150,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case
|
||||
]
|
||||
knownGroup prepared = grpOrBizLink g <> ": known " <> prepared <> grpOrBiz g <> " " <> ttyGroup' g
|
||||
GLPNoRelays _ -> [grpLink "channel has no active relays, please try to join later"]
|
||||
GLPUpdateRequired _ -> [grpLink "this group requires a newer version of the app, please upgrade"]
|
||||
where
|
||||
connecting g = [grpOrBizLink g <> ": connecting to " <> grpOrBiz g <> " " <> ttyGroup' g]
|
||||
grpLink = ("group link: " <>)
|
||||
|
||||
+40
-5
@@ -10,6 +10,7 @@ import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Simplex.Chat.Markdown
|
||||
import Simplex.Messaging.Agent.Protocol (SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..))
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Util ((<$$>))
|
||||
import System.Console.ANSI.Types
|
||||
@@ -28,6 +29,7 @@ markdownTests = do
|
||||
textWithPhone
|
||||
textWithMentions
|
||||
textWithCommands
|
||||
textWithSimplexNames
|
||||
multilineMarkdownList
|
||||
testSanitizeUri
|
||||
|
||||
@@ -117,7 +119,7 @@ secretText = describe "secret text" do
|
||||
"this is # unformatted # text"
|
||||
<==> "this is # unformatted # text"
|
||||
"this is #unformatted # text"
|
||||
<==> "this is #unformatted # text"
|
||||
<==> "this is " <> sname NTPublicGroup TLDSimplex "unformatted" [] "unformatted" <> " # text"
|
||||
"this is # unformatted# text"
|
||||
<==> "this is # unformatted# text"
|
||||
"this is ## unformatted ## text"
|
||||
@@ -125,9 +127,9 @@ secretText = describe "secret text" do
|
||||
"this is#unformatted# text"
|
||||
<==> "this is#unformatted# text"
|
||||
"this is #unformatted text"
|
||||
<==> "this is #unformatted text"
|
||||
<==> "this is " <> sname NTPublicGroup TLDSimplex "unformatted" [] "unformatted" <> " text"
|
||||
"*this* is #unformatted text"
|
||||
<==> bold "this" <> " is #unformatted text"
|
||||
<==> bold "this" <> " is " <> sname NTPublicGroup TLDSimplex "unformatted" [] "unformatted" <> " text"
|
||||
it "ignored internal markdown" do
|
||||
"snippet: `this is #secret_text#`"
|
||||
<==> "snippet: " <> markdown Snippet "this is #secret_text#"
|
||||
@@ -297,8 +299,8 @@ textWithEmail = describe "text with Email" do
|
||||
"test chat@simplex.chat." <==> "test " <> email "chat@simplex.chat" <> "."
|
||||
"test chat@simplex.chat..." <==> "test " <> email "chat@simplex.chat" <> "..."
|
||||
it "ignored as email markdown" do
|
||||
"chat @simplex.chat" <==> "chat " <> mention "simplex.chat" "@simplex.chat"
|
||||
"this is chat @simplex.chat" <==> "this is chat " <> mention "simplex.chat" "@simplex.chat"
|
||||
"chat @simplex.chat" <==> "chat " <> sname NTContact TLDWeb "simplex.chat" [] "simplex.chat"
|
||||
"this is chat @simplex.chat" <==> "this is chat " <> sname NTContact TLDWeb "simplex.chat" [] "simplex.chat"
|
||||
"this is chat@ simplex.chat" <==> "this is chat@ " <> uri "simplex.chat"
|
||||
"this is chat @ simplex.chat" <==> "this is chat @ " <> uri "simplex.chat"
|
||||
"*this* is chat @ simplex.chat" <==> bold "this" <> " is chat @ " <> uri "simplex.chat"
|
||||
@@ -378,6 +380,39 @@ uri' = FormattedText $ Just Uri
|
||||
command' :: Text -> Text -> FormattedText
|
||||
command' = FormattedText . Just . Command
|
||||
|
||||
sname :: SimplexNameType -> SimplexTLD -> Text -> [Text] -> Text -> Markdown
|
||||
sname nt ns dom sub txt = markdown (SimplexName $ SimplexNameInfo nt ns dom sub) (pfx <> txt)
|
||||
where
|
||||
pfx = case nt of NTPublicGroup -> "#"; NTContact -> "@"
|
||||
|
||||
textWithSimplexNames :: Spec
|
||||
textWithSimplexNames = describe "text with SimpleX names" do
|
||||
it "channel names - simplex namespace" do
|
||||
"#privacy" <==> sname NTPublicGroup TLDSimplex "privacy" [] "privacy"
|
||||
"#privacy.simplex" <==> sname NTPublicGroup TLDSimplex "privacy" [] "privacy.simplex"
|
||||
"#my-channel.simplex" <==> sname NTPublicGroup TLDSimplex "my-channel" [] "my-channel.simplex"
|
||||
"hello #privacy!" <==> "hello " <> sname NTPublicGroup TLDSimplex "privacy" [] "privacy" <> "!"
|
||||
"see #privacy.simplex now" <==> "see " <> sname NTPublicGroup TLDSimplex "privacy" [] "privacy.simplex" <> " now"
|
||||
"#123" <==> sname NTPublicGroup TLDSimplex "123" [] "123"
|
||||
it "channel names - subdomains" do
|
||||
"#support.acme.simplex" <==> sname NTPublicGroup TLDSimplex "acme" ["support"] "support.acme.simplex"
|
||||
"#a.b.acme.simplex" <==> sname NTPublicGroup TLDSimplex "acme" ["b", "a"] "a.b.acme.simplex"
|
||||
it "channel names - testing namespace" do
|
||||
"#test.testing" <==> sname NTPublicGroup TLDTesting "test" [] "test.testing"
|
||||
"#sub.test.testing" <==> sname NTPublicGroup TLDTesting "test" ["sub"] "sub.test.testing"
|
||||
it "channel names - web domains" do
|
||||
"#example.com" <==> sname NTPublicGroup TLDWeb "example.com" [] "example.com"
|
||||
"#news.bbc.co.uk" <==> sname NTPublicGroup TLDWeb "news.bbc.co.uk" [] "news.bbc.co.uk"
|
||||
"#123.com" <==> sname NTPublicGroup TLDWeb "123.com" [] "123.com"
|
||||
it "contact names" do
|
||||
"@privacy.simplex" <==> sname NTContact TLDSimplex "privacy" [] "privacy.simplex"
|
||||
"@my-name.simplex" <==> sname NTContact TLDSimplex "my-name" [] "my-name.simplex"
|
||||
"@alice.example.com" <==> sname NTContact TLDWeb "alice.example.com" [] "alice.example.com"
|
||||
it "not parsed as names" do
|
||||
"#secret#" <==> markdown Secret "secret"
|
||||
"##double secret##" <==> markdown Secret "#double secret#"
|
||||
"#" <==> "#"
|
||||
|
||||
multilineMarkdownList :: Spec
|
||||
multilineMarkdownList = describe "multiline markdown" do
|
||||
it "correct markdown" do
|
||||
|
||||
+33
-16
@@ -10,15 +10,17 @@ validNameTests = describe "valid chat names" $ do
|
||||
testMkValidName :: IO ()
|
||||
testMkValidName = do
|
||||
mkValidName "alice" `shouldBe` "alice"
|
||||
mkValidName " alice" `shouldBe` "alice"
|
||||
mkValidName "?alice" `shouldBe` "alice"
|
||||
mkValidName "алиса" `shouldBe` "алиса"
|
||||
mkValidName "John Doe" `shouldBe` "John Doe"
|
||||
mkValidName "J.Doe" `shouldBe` "J.Doe"
|
||||
mkValidName "J. Doe" `shouldBe` "J. Doe"
|
||||
mkValidName "J..Doe" `shouldBe` "J..Doe"
|
||||
mkValidName "J ..Doe" `shouldBe` "J ..Doe"
|
||||
mkValidName "J ... Doe" `shouldBe` "J ... Doe"
|
||||
mkValidName "J .... Doe" `shouldBe` "J ... Doe"
|
||||
mkValidName "J . . Doe" `shouldBe` "J . Doe"
|
||||
mkValidName "J.Doe" `shouldBe` "JDoe"
|
||||
mkValidName "J. Doe" `shouldBe` "J Doe"
|
||||
mkValidName "J..Doe" `shouldBe` "JDoe"
|
||||
mkValidName "J ..Doe" `shouldBe` "J Doe"
|
||||
mkValidName "J ... Doe" `shouldBe` "J Doe"
|
||||
mkValidName "J .... Doe" `shouldBe` "J Doe"
|
||||
mkValidName "J . . Doe" `shouldBe` "J Doe"
|
||||
mkValidName "@alice" `shouldBe` "alice"
|
||||
mkValidName "#alice" `shouldBe` "alice"
|
||||
mkValidName "'alice" `shouldBe` "alice"
|
||||
@@ -26,17 +28,32 @@ testMkValidName = do
|
||||
mkValidName "alice " `shouldBe` "alice"
|
||||
mkValidName "John Doe" `shouldBe` "John Doe"
|
||||
mkValidName "'John Doe'" `shouldBe` "John Doe"
|
||||
mkValidName "\"John Doe\"" `shouldBe` "John Doe\""
|
||||
mkValidName "`John Doe`" `shouldBe` "`John Doe`"
|
||||
mkValidName "John \"Doe\"" `shouldBe` "John \"Doe\""
|
||||
mkValidName "John `Doe`" `shouldBe` "John `Doe`"
|
||||
mkValidName "alice/bob" `shouldBe` "alice/bob"
|
||||
mkValidName "alice / bob" `shouldBe` "alice / bob"
|
||||
mkValidName "alice /// bob" `shouldBe` "alice /// bob"
|
||||
mkValidName "alice //// bob" `shouldBe` "alice /// bob"
|
||||
mkValidName "\"John Doe\"" `shouldBe` "John Doe"
|
||||
mkValidName "`John Doe`" `shouldBe` "John Doe"
|
||||
mkValidName "John \"Doe\"" `shouldBe` "John Doe"
|
||||
mkValidName "John `Doe`" `shouldBe` "John Doe"
|
||||
mkValidName "alice/bob" `shouldBe` "alicebob"
|
||||
mkValidName "alice / bob" `shouldBe` "alice bob"
|
||||
mkValidName "alice /// bob" `shouldBe` "alice bob"
|
||||
mkValidName "alice //// bob" `shouldBe` "alice bob"
|
||||
mkValidName "alice >>= bob" `shouldBe` "alice >>= bob"
|
||||
mkValidName "alice@example.com" `shouldBe` "alice@example.com"
|
||||
mkValidName "alice@example.com" `shouldBe` "aliceexamplecom"
|
||||
mkValidName "alice <> bob" `shouldBe` "alice <> bob"
|
||||
mkValidName "alice -> bob" `shouldBe` "alice -> bob"
|
||||
mkValidName "alice & bob" `shouldBe` "alice & bob"
|
||||
mkValidName "alice && bob" `shouldBe` "alice & bob"
|
||||
mkValidName "alice & & bob" `shouldBe` "alice & bob"
|
||||
mkValidName "alice-bob" `shouldBe` "alice-bob"
|
||||
mkValidName "alice--bob" `shouldBe` "alice-bob"
|
||||
mkValidName "alice -- bob" `shouldBe` "alice - bob"
|
||||
mkValidName "alice \\ bob" `shouldBe` "alice bob"
|
||||
mkValidName "alice (bob)" `shouldBe` "alice bob"
|
||||
mkValidName "alice: bob" `shouldBe` "alice: bob"
|
||||
mkValidName "alice 👍" `shouldBe` "alice 👍"
|
||||
mkValidName "👍" `shouldBe` "👍"
|
||||
mkValidName "alice >" `shouldBe` "alice >"
|
||||
mkValidName "> alice" `shouldBe` "alice"
|
||||
mkValidName "123" `shouldBe` "123"
|
||||
mkValidName "123 alice" `shouldBe` "123 alice"
|
||||
mkValidName "01234567890123456789012345678901234567890123456789extra" `shouldBe` "01234567890123456789012345678901234567890123456789"
|
||||
mkValidName "0123456789012345678901234567890123456789012345678 extra" `shouldBe` "0123456789012345678901234567890123456789012345678"
|
||||
|
||||
Reference in New Issue
Block a user