mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3267b4d6ca | |||
| 9b302b856a | |||
| 4e696aed82 | |||
| 425c7b947f | |||
| d4f9429fc1 | |||
| 161b43e85d | |||
| d585e8f5a7 | |||
| 060e7cdf52 | |||
| 6fa002948e | |||
| bbd4e6c8ba | |||
| 92cf945e10 | |||
| cc0f55c245 | |||
| 22f27c4255 | |||
| 14a888bf43 | |||
| f6fddc9436 | |||
| f581e91f19 | |||
| fb72dfcdee | |||
| 925813b14c | |||
| abd410fe62 | |||
| 875282e9ec | |||
| 6afda28367 | |||
| 0721b24250 | |||
| 10b6bce8a2 | |||
| 0101444c5d | |||
| 128883b8a3 | |||
| cc75b75d4e | |||
| dea6cd81c7 | |||
| 2f53ab08b5 |
@@ -1,7 +1,7 @@
|
||||
name: Bug
|
||||
description: File a bug report/issue
|
||||
title: "[Bug]: "
|
||||
labels: ["type:bug", "type:triage"]
|
||||
labels: ["bug", "triage"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Feature
|
||||
description: Suggest your feature
|
||||
title: "[Feature]: "
|
||||
labels: ["type:enhancement", "type:triage"]
|
||||
labels: ["enhancement", "triage"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Question
|
||||
description: Ask your question
|
||||
title: "[Q]: "
|
||||
labels: ["type:question", "type:triage"]
|
||||
labels: ["question", "triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
targetSdk 32
|
||||
// !!!
|
||||
// skip version code after release to F-Droid, as it uses two version codes
|
||||
versionCode 125
|
||||
versionName "5.1.2"
|
||||
versionCode 127
|
||||
versionName "5.1.3"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -1624,8 +1624,9 @@ fun getTimestampText(t: Instant): String {
|
||||
val tz = TimeZone.currentSystemDefault()
|
||||
val now: LocalDateTime = Clock.System.now().toLocalDateTime(tz)
|
||||
val time: LocalDateTime = t.toLocalDateTime(tz)
|
||||
val period = now.date.minus(time.date)
|
||||
val recent = now.date == time.date ||
|
||||
(now.date.minus(time.date).days == 1 && now.hour < 12 && time.hour >= 18 )
|
||||
(period.years == 0 && period.months == 0 && period.days == 1 && now.hour < 12 && time.hour >= 18 )
|
||||
val dateFormatter =
|
||||
if (recent) {
|
||||
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
|
||||
@@ -1480,10 +1480,14 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(r.groupInfo, r.member)
|
||||
}
|
||||
is CR.ConnectedToGroupMember ->
|
||||
is CR.ConnectedToGroupMember -> {
|
||||
if (active(r.user)) {
|
||||
chatModel.upsertGroupMember(r.groupInfo, r.member)
|
||||
}
|
||||
if (r.memberContact != null) {
|
||||
chatModel.setContactNetworkStatus(r.memberContact, NetworkStatus.Connected())
|
||||
}
|
||||
}
|
||||
is CR.GroupUpdated ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroup(r.toGroup)
|
||||
@@ -3333,7 +3337,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("groupInvitation") class GroupInvitation(val user: User, val groupInfo: GroupInfo): CR() // unused
|
||||
@Serializable @SerialName("userJoinedGroup") class UserJoinedGroup(val user: User, val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val memberContact: Contact? = null): CR()
|
||||
@Serializable @SerialName("groupRemoved") class GroupRemoved(val user: User, val groupInfo: GroupInfo): CR() // unused
|
||||
@Serializable @SerialName("groupUpdated") class GroupUpdated(val user: User, val toGroup: GroupInfo): CR()
|
||||
@Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: User, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR()
|
||||
@@ -3558,7 +3562,7 @@ sealed class CR {
|
||||
is GroupInvitation -> withUser(user, json.encodeToString(groupInfo))
|
||||
is UserJoinedGroup -> withUser(user, json.encodeToString(groupInfo))
|
||||
is JoinedGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
|
||||
is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
|
||||
is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nmemberContact: $memberContact")
|
||||
is GroupRemoved -> withUser(user, json.encodeToString(groupInfo))
|
||||
is GroupUpdated -> withUser(user, json.encodeToString(toGroup))
|
||||
is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole")
|
||||
|
||||
@@ -159,6 +159,14 @@ fun GroupMenuItems(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showM
|
||||
DeleteGroupAction(chat, groupInfo, chatModel, showMenu)
|
||||
}
|
||||
}
|
||||
GroupMemberStatus.MemAccepted -> {
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
LeaveGroupAction(groupInfo, chatModel, showMenu)
|
||||
}
|
||||
if (groupInfo.canDelete) {
|
||||
DeleteGroupAction(chat, groupInfo, chatModel, showMenu)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (showMarkRead) {
|
||||
MarkReadChatAction(chat, chatModel, showMenu)
|
||||
|
||||
@@ -473,7 +473,7 @@
|
||||
<string name="v4_5_reduced_battery_usage_descr">Meer verbeteringen volgen snel!</string>
|
||||
<string name="button_add_members">Nodig leden uit</string>
|
||||
<string name="notification_display_mode_hidden_desc">Verberg contact en bericht</string>
|
||||
<string name="turn_off_battery_optimization">Om het te gebruiken <b>batterijoptimalisatie uitschakelen</b> voor <xliff:g xmlns:xliff="urn:oasis: names:tc:xliff:document:1.2" id="appName">SimpleX</xliff:g> in het volgende dialoogvenster. Anders worden de meldingen uitgeschakeld.</string>
|
||||
<string name="turn_off_battery_optimization">Om het te gebruiken <b>batterijoptimalisatie uitschakelen</b> voor <xliff:g id="appName">SimpleX</xliff:g> in het volgende dialoogvenster. Anders worden de meldingen uitgeschakeld.</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Als u ervoor kiest om te weigeren, wordt de afzender NIET op de hoogte gesteld.</string>
|
||||
<string name="onboarding_notifications_mode_service">Onmiddellijk</string>
|
||||
<string name="rcv_group_event_member_added">heeft <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> uitgenodigd</string>
|
||||
|
||||
@@ -1345,10 +1345,13 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
case let .connectedToGroupMember(user, groupInfo, member):
|
||||
case let .connectedToGroupMember(user, groupInfo, member, memberContact):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
if let contact = memberContact {
|
||||
m.setContactNetworkStatus(contact, .connected)
|
||||
}
|
||||
case let .groupUpdated(user, toGroup):
|
||||
if active(user) {
|
||||
m.updateGroup(toGroup)
|
||||
|
||||
@@ -13,6 +13,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
||||
RTCInitializeSSL()
|
||||
let videoEncoderFactory = RTCDefaultVideoEncoderFactory()
|
||||
let videoDecoderFactory = RTCDefaultVideoDecoderFactory()
|
||||
videoEncoderFactory.preferredCodec = RTCVideoCodecInfo(name: kRTCVp8CodecName)
|
||||
return RTCPeerConnectionFactory(encoderFactory: videoEncoderFactory, decoderFactory: videoDecoderFactory)
|
||||
}()
|
||||
private static let ivTagBytes: Int = 28
|
||||
@@ -301,6 +302,17 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
||||
}
|
||||
|
||||
func startCaptureLocalVideo(_ activeCall: Call) {
|
||||
#if targetEnvironment(simulator)
|
||||
guard
|
||||
let capturer = activeCall.localCamera as? RTCFileVideoCapturer
|
||||
else {
|
||||
logger.error("Unable to work with a file capturer")
|
||||
return
|
||||
}
|
||||
capturer.stopCapture()
|
||||
// Drag video file named `video.mp4` to `sounds` directory in the project from any other path in filesystem
|
||||
capturer.startCapturing(fromFileNamed: "sounds/video.mp4")
|
||||
#else
|
||||
guard
|
||||
let capturer = activeCall.localCamera as? RTCCameraVideoCapturer,
|
||||
let camera = (RTCCameraVideoCapturer.captureDevices().first { $0.position == activeCall.device })
|
||||
@@ -328,6 +340,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
||||
capturer.startCapture(with: camera,
|
||||
format: format,
|
||||
fps: Int(min(24, fps.maxFrameRate)))
|
||||
#endif
|
||||
}
|
||||
|
||||
private func createAudioSender(_ connection: RTCPeerConnection) {
|
||||
|
||||
@@ -108,6 +108,14 @@ struct ChatListNavLink: View {
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(groupInvitationAcceptedAlert())
|
||||
}
|
||||
.swipeActions(edge: .trailing) {
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
leaveGroupChatButton(groupInfo)
|
||||
}
|
||||
if groupInfo.canDelete {
|
||||
deleteGroupChatButton(groupInfo)
|
||||
}
|
||||
}
|
||||
default:
|
||||
NavLinkPlain(
|
||||
tag: chat.chatInfo.id,
|
||||
@@ -124,12 +132,7 @@ struct ChatListNavLink: View {
|
||||
clearChatButton()
|
||||
}
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
Button {
|
||||
AlertManager.shared.showAlert(leaveGroupAlert(groupInfo))
|
||||
} label: {
|
||||
Label("Leave", systemImage: "rectangle.portrait.and.arrow.right")
|
||||
}
|
||||
.tint(Color.yellow)
|
||||
leaveGroupChatButton(groupInfo)
|
||||
}
|
||||
}
|
||||
.swipeActions(edge: .trailing) {
|
||||
@@ -177,7 +180,16 @@ struct ChatListNavLink: View {
|
||||
.tint(Color.orange)
|
||||
}
|
||||
|
||||
@ViewBuilder private func deleteGroupChatButton(_ groupInfo: GroupInfo) -> some View {
|
||||
private func leaveGroupChatButton(_ groupInfo: GroupInfo) -> some View {
|
||||
Button {
|
||||
AlertManager.shared.showAlert(leaveGroupAlert(groupInfo))
|
||||
} label: {
|
||||
Label("Leave", systemImage: "rectangle.portrait.and.arrow.right")
|
||||
}
|
||||
.tint(Color.yellow)
|
||||
}
|
||||
|
||||
private func deleteGroupChatButton(_ groupInfo: GroupInfo) -> some View {
|
||||
Button {
|
||||
AlertManager.shared.showAlert(deleteGroupAlert(groupInfo))
|
||||
} label: {
|
||||
|
||||
@@ -83,11 +83,6 @@
|
||||
5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */; };
|
||||
5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059C4279559F40002BEB4 /* ContentView.swift */; };
|
||||
5CA059EF279559F40002BEB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5CA059C5279559F40002BEB4 /* Assets.xcassets */; };
|
||||
5CA4874A2A228AF400409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA487452A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a */; };
|
||||
5CA4874B2A228AF400409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA487462A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a */; };
|
||||
5CA4874C2A228AF400409F23 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA487472A228AF300409F23 /* libgmp.a */; };
|
||||
5CA4874D2A228AF400409F23 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA487482A228AF300409F23 /* libffi.a */; };
|
||||
5CA4874E2A228AF400409F23 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA487492A228AF400409F23 /* libgmpxx.a */; };
|
||||
5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA7DFC229302AF000F7FDDE /* AppSheet.swift */; };
|
||||
5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79929211BB900072E13 /* PreferencesView.swift */; };
|
||||
5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */; };
|
||||
@@ -152,6 +147,11 @@
|
||||
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||
6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; };
|
||||
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
|
||||
643EE9682A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643EE9632A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt.a */; };
|
||||
643EE9692A372E8700678085 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643EE9642A372E8700678085 /* libffi.a */; };
|
||||
643EE96A2A372E8700678085 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643EE9652A372E8700678085 /* libgmpxx.a */; };
|
||||
643EE96B2A372E8700678085 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643EE9662A372E8700678085 /* libgmp.a */; };
|
||||
643EE96C2A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643EE9672A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt-ghc8.10.7.a */; };
|
||||
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
|
||||
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
|
||||
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; };
|
||||
@@ -344,11 +344,6 @@
|
||||
5CA059D7279559F40002BEB4 /* Tests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5CA059DB279559F40002BEB4 /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = "<group>"; };
|
||||
5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOSLaunchTests.swift; sourceTree = "<group>"; };
|
||||
5CA487452A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5CA487462A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a"; sourceTree = "<group>"; };
|
||||
5CA487472A228AF300409F23 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CA487482A228AF300409F23 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CA487492A228AF400409F23 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CA7DFC229302AF000F7FDDE /* AppSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSheet.swift; sourceTree = "<group>"; };
|
||||
5CA85D0A297218AA0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5CA85D0B297218AA0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@@ -427,6 +422,11 @@
|
||||
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
|
||||
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; };
|
||||
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
|
||||
643EE9632A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt.a"; sourceTree = "<group>"; };
|
||||
643EE9642A372E8700678085 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
643EE9652A372E8700678085 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
643EE9662A372E8700678085 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
643EE9672A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
|
||||
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
|
||||
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = "<group>"; };
|
||||
@@ -498,12 +498,12 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5CA4874D2A228AF400409F23 /* libffi.a in Frameworks */,
|
||||
5CA4874C2A228AF400409F23 /* libgmp.a in Frameworks */,
|
||||
5CA4874B2A228AF400409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a in Frameworks */,
|
||||
5CA4874A2A228AF400409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a in Frameworks */,
|
||||
643EE96A2A372E8700678085 /* libgmpxx.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5CA4874E2A228AF400409F23 /* libgmpxx.a in Frameworks */,
|
||||
643EE9682A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt.a in Frameworks */,
|
||||
643EE96B2A372E8700678085 /* libgmp.a in Frameworks */,
|
||||
643EE9692A372E8700678085 /* libffi.a in Frameworks */,
|
||||
643EE96C2A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt-ghc8.10.7.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -564,11 +564,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CA487482A228AF300409F23 /* libffi.a */,
|
||||
5CA487472A228AF300409F23 /* libgmp.a */,
|
||||
5CA487492A228AF400409F23 /* libgmpxx.a */,
|
||||
5CA487452A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a */,
|
||||
5CA487462A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a */,
|
||||
643EE9642A372E8700678085 /* libffi.a */,
|
||||
643EE9662A372E8700678085 /* libgmp.a */,
|
||||
643EE9652A372E8700678085 /* libgmpxx.a */,
|
||||
643EE9672A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt-ghc8.10.7.a */,
|
||||
643EE9632A372E8700678085 /* libHSsimplex-chat-5.1.3.0-7OTLHAmjBPvIoz7MIh3bdt.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -1470,7 +1470,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 149;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1491,7 +1491,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.1.2;
|
||||
MARKETING_VERSION = 5.1.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1512,7 +1512,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 149;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1533,7 +1533,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.1.2;
|
||||
MARKETING_VERSION = 5.1.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1592,7 +1592,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 149;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1605,7 +1605,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.1.2;
|
||||
MARKETING_VERSION = 5.1.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -1624,7 +1624,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 149;
|
||||
CURRENT_PROJECT_VERSION = 150;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1637,7 +1637,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.1.2;
|
||||
MARKETING_VERSION = 5.1.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@@ -456,7 +456,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case groupInvitation(user: User, groupInfo: GroupInfo) // unused
|
||||
case userJoinedGroup(user: User, groupInfo: GroupInfo)
|
||||
case joinedGroupMember(user: User, groupInfo: GroupInfo, member: GroupMember)
|
||||
case connectedToGroupMember(user: User, groupInfo: GroupInfo, member: GroupMember)
|
||||
case connectedToGroupMember(user: User, groupInfo: GroupInfo, member: GroupMember, memberContact: Contact?)
|
||||
case groupRemoved(user: User, groupInfo: GroupInfo) // unused
|
||||
case groupUpdated(user: User, toGroup: GroupInfo)
|
||||
case groupLinkCreated(user: User, groupInfo: GroupInfo, connReqContact: String, memberRole: GroupMemberRole)
|
||||
@@ -692,7 +692,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .groupInvitation(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .userJoinedGroup(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .joinedGroupMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .connectedToGroupMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .connectedToGroupMember(u, groupInfo, member, memberContact): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nmemberContact: \(String(describing: memberContact))")
|
||||
case let .groupRemoved(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
|
||||
case let .groupLinkCreated(u, groupInfo, connReqContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)\nmemberRole: \(memberRole)")
|
||||
|
||||
@@ -2283,10 +2283,22 @@ let msgTimeFormat = Date.FormatStyle.dateTime.hour().minute()
|
||||
let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits)
|
||||
|
||||
public func formatTimestampText(_ date: Date) -> Text {
|
||||
let now = Calendar.current.dateComponents([.day, .hour], from: .now)
|
||||
let dc = Calendar.current.dateComponents([.day, .hour], from: date)
|
||||
let recent = now.day == dc.day || ((now.day ?? 0) - (dc.day ?? 0) == 1 && (dc.hour ?? 0) >= 18 && (now.hour ?? 0) < 12)
|
||||
return Text(date, format: recent ? msgTimeFormat : msgDateFormat)
|
||||
return Text(date, format: recent(date) ? msgTimeFormat : msgDateFormat)
|
||||
}
|
||||
|
||||
private func recent(_ date: Date) -> Bool {
|
||||
let now = Date()
|
||||
let calendar = Calendar.current
|
||||
|
||||
guard let previousDay = calendar.date(byAdding: DateComponents(day: -1), to: now),
|
||||
let previousDay18 = calendar.date(bySettingHour: 18, minute: 0, second: 0, of: previousDay),
|
||||
let currentDay00 = calendar.date(bySettingHour: 0, minute: 0, second: 0, of: now),
|
||||
let currentDay12 = calendar.date(bySettingHour: 12, minute: 0, second: 0, of: now) else {
|
||||
return false
|
||||
}
|
||||
|
||||
let isSameDay = calendar.isDate(date, inSameDayAs: now)
|
||||
return isSameDay || (now < currentDay12 && date >= previousDay18 && date < currentDay00)
|
||||
}
|
||||
|
||||
public enum CIStatus: Decodable {
|
||||
|
||||
+2
-2
@@ -30,7 +30,7 @@ In a more narrow sense, particularly in media, blockchain is used to refer speci
|
||||
|
||||
Centralized networks are provided or controlled by a single entity. The examples are Threema, Signal, WhatsApp and Telegram. The advantage of that design is that the provider can innovate faster, and has a centralized approach to security. But the disadvantage is that the provider can change or discontinue the service, and leak, sell or disclose in some other way all users' data, including who they are connected with.
|
||||
|
||||
## Content padding
|
||||
## Content padding
|
||||
|
||||
[Message padding](#message-padding).
|
||||
|
||||
@@ -149,7 +149,7 @@ Generalizing [the definition](https://csrc.nist.gov/glossary/term/pairwise_pseud
|
||||
|
||||
In the context of SimpleX network, these are the identifiers generated by SMP relays to access anonymous messaging queues, with a separate identifier (and access credential) for each accessing party: recipient, sender and and optional notifications subscriber. The same approach is used by XFTP relays to access file chunks, with separate identifiers (and access credentials) for sender and each recipient.
|
||||
|
||||
## Peer-to-peer
|
||||
## Peer-to-peer
|
||||
|
||||
Peer-to-peer (P2P) is the network architecture when participants have equal rights and communicate directly via a general purpose transport or overlay network. Unlike client-server architecture, all peers in a P2P network both provide and consume the resources. In the context of messaging, P2P architecture usually means that the messages are sent between peers, without user accounts or messages being stored on any servers. Examples are Tox, Briar, Cwtch and many others.
|
||||
|
||||
|
||||
+7
-4
@@ -1,15 +1,15 @@
|
||||
---
|
||||
title: Hosting your own SMP Server
|
||||
revision: 31.01.2023
|
||||
revision: 05.06.2023
|
||||
---
|
||||
|
||||
| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) |
|
||||
| Updated 05.06.2023 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) |
|
||||
|
||||
# Hosting your own SMP Server
|
||||
|
||||
## Overview
|
||||
|
||||
SMP server is the relay server used to pass messages in SimpleX network. SimpleX Chat apps have preset servers (for mobile apps these are smp8, smp9 and smp10.simplex.im), but you can easily change app configuration to use other servers.
|
||||
SMP server is the relay server used to pass messages in SimpleX network. SimpleX Chat apps have preset servers (for mobile apps these are smp11, smp12 and smp14.simplex.im), but you can easily change app configuration to use other servers.
|
||||
|
||||
SimpleX clients only determine which server is used to receive the messages, separately for each contact (or group connection with a group member), and these servers are only temporary, as the delivery address can change.
|
||||
|
||||
@@ -47,7 +47,10 @@ Manual installation requires some preliminary actions:
|
||||
|
||||
```sh
|
||||
# For Ubuntu
|
||||
ufw allow 5223
|
||||
sudo ufw allow 5223/tcp
|
||||
# For Fedora
|
||||
sudo firewall-cmd --permanent --add-port=5223/tcp && \
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
4. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/smp-server.service` file with the following content:
|
||||
|
||||
+15
-5
@@ -74,11 +74,21 @@ systemctl enable coturn && systemctl start coturn
|
||||
- **49152:65535** – port range that Coturn will use by default for TURN relay.
|
||||
|
||||
```sh
|
||||
ufw allow 3478 && \
|
||||
ufw allow 443 && \
|
||||
ufw allow 5349 && \
|
||||
ufw allow 49152:65535/tcp && \
|
||||
ufw allow 49152:65535/udp
|
||||
# For Ubuntu
|
||||
sudo ufw allow 3478 && \
|
||||
sudo ufw allow 443 && \
|
||||
sudo ufw allow 5349 && \
|
||||
sudo ufw allow 49152:65535/tcp && \
|
||||
sudo ufw allow 49152:65535/udp
|
||||
|
||||
# For Fedora
|
||||
sudo firewall-cmd --permanent --add-port=443/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=443/udp && \
|
||||
sudo firewall-cmd --permanent --add-port=5349/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=5349/udp && \
|
||||
sudo firewall-cmd --permanent --add-port=49152:65535/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=49152:65535/udp && \
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
## Configure mobile apps
|
||||
|
||||
+4
-1
@@ -40,7 +40,10 @@ XFTP is a new file transfer protocol focussed on meta-data protection - it is ba
|
||||
|
||||
```sh
|
||||
# For Ubuntu
|
||||
sudo ufw allow 443
|
||||
sudo ufw allow 443/tcp
|
||||
# For Fedora
|
||||
sudo firewall-cmd --permanent --add-port=443/tcp && \
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
5. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/xftp-server.service` file with the following content:
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
title: Hostování vlastního serveru SMP
|
||||
revision: 31.01.2023
|
||||
revision: 05.06.2023
|
||||
---
|
||||
| Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/SERVER.md), [FR](/docs/lang/fr/SERVER.md) |
|
||||
| Aktualizováno 05.06.2023 | Jazyky: CZ, [EN](/docs/SERVER.md), [FR](/docs/lang/fr/SERVER.md) |
|
||||
|
||||
# Hostování vlastního serveru SMP
|
||||
|
||||
## Přehled
|
||||
|
||||
SMP server je relay server používaný k předávání zpráv v síti SimpleX. Aplikace SimpleX Chat mají přednastavené servery (pro mobilní aplikace jsou to smp8, smp9 a smp10.simplex.im), ale konfiguraci aplikace můžete snadno změnit a používat jiné servery.
|
||||
SMP server je relay server používaný k předávání zpráv v síti SimpleX. Aplikace SimpleX Chat mají přednastavené servery (pro mobilní aplikace jsou to smp11, smp12 a smp14.simplex.im), ale konfiguraci aplikace můžete snadno změnit a používat jiné servery.
|
||||
|
||||
Klienti SimpleX pouze určují, který server bude použit pro příjem zpráv, a to pro každý kontakt (nebo spojení skupiny s členem skupiny) zvlášť, přičemž tyto servery jsou pouze dočasné, protože adresa pro doručování se může změnit.
|
||||
|
||||
@@ -46,7 +46,10 @@ Ruční instalace vyžaduje několik předběžných úkonů:
|
||||
|
||||
```sh
|
||||
# Pro Ubuntu
|
||||
ufw allow 5233
|
||||
sudo ufw allow 5233/tcp
|
||||
# Pro Fedora
|
||||
sudo firewall-cmd --permanent --add-port=5223/tcp && \
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
4. **Volitelné** - Pokud používáte distribuci s `systemd`, vytvořte soubor `/etc/systemd/system/smp-server.service` s následujícím obsahem:
|
||||
|
||||
+15
-5
@@ -73,11 +73,21 @@ systemctl enable coturn && systemctl start coturn
|
||||
- **49152:65535** - rozsah portů, který bude společnost Coturn ve výchozím nastavení používat pro přenos TURN.
|
||||
|
||||
```sh
|
||||
ufw allow 3478 && \
|
||||
ufw allow 443 && \
|
||||
ufw allow 5349 && \
|
||||
ufw allow 49152:65535/tcp && \
|
||||
ufw allow 49152:65535/udp
|
||||
# Pro Ubuntu
|
||||
sudo ufw allow 3478 && \
|
||||
sudo ufw allow 443 && \
|
||||
sudo ufw allow 5349 && \
|
||||
sudo ufw allow 49152:65535/tcp && \
|
||||
sudo ufw allow 49152:65535/udp
|
||||
|
||||
# Pro Fedora
|
||||
sudo firewall-cmd --permanent --add-port=443/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=443/udp && \
|
||||
sudo firewall-cmd --permanent --add-port=5349/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=5349/udp && \
|
||||
sudo firewall-cmd --permanent --add-port=49152:65535/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=49152:65535/udp && \
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
## Konfigurace mobilních aplikací
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
title: Héberger votre propre serveur SMP
|
||||
revision: 31.01.2023
|
||||
revision: 05.06.2023
|
||||
---
|
||||
| 31.01.2023 | FR, [EN](/docs/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) |
|
||||
| 05.06.2023 | FR, [EN](/docs/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) |
|
||||
|
||||
# Héberger votre propre serveur SMP
|
||||
|
||||
## Présentation générale
|
||||
|
||||
Un serveur SMP est un serveur relais utilisé pour transmettre les messages sur le réseau SimpleX. Les apps SimpleX Chat ont des serveurs prédéfinis (pour les apps mobiles, smp8, smp9 et smp10.simplex.im), mais vous pouvez facilement modifier la configuration de l'app pour utiliser d'autres serveurs.
|
||||
Un serveur SMP est un serveur relais utilisé pour transmettre les messages sur le réseau SimpleX. Les apps SimpleX Chat ont des serveurs prédéfinis (pour les apps mobiles, smp11, smp12 et smp14.simplex.im), mais vous pouvez facilement modifier la configuration de l'app pour utiliser d'autres serveurs.
|
||||
|
||||
Seuls les utilisateurs de SimpleX déterminent quel serveur est utilisé pour recevoir les messages, séparément pour chaque contact (ou pour chaque connexion à un membre d'un groupe), et ces serveurs ne sont que temporaires, car l'adresse de réception peut changer.
|
||||
|
||||
@@ -46,7 +46,10 @@ L'installation manuelle nécessite quelques actions préalables :
|
||||
|
||||
```sh
|
||||
# Pour Ubuntu
|
||||
ufw allow 5223
|
||||
sudo ufw allow 5223/tcp
|
||||
# Pour Fedora
|
||||
sudo firewall-cmd --permanent --add-port=5223/tcp && \
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
4. **Optionnel** - Si vous utilisez une distribution avec `systemd`, créez le fichier `/etc/systemd/system/smp-server.service` avec le contenu suivant :
|
||||
|
||||
+15
-5
@@ -73,11 +73,21 @@ systemctl enable coturn && systemctl start coturn
|
||||
- **49152:65535** – plage de ports que Coturn utilisera par défaut pour le relais TURN.
|
||||
|
||||
```sh
|
||||
ufw allow 3478 && \
|
||||
ufw allow 443 && \
|
||||
ufw allow 5349 && \
|
||||
ufw allow 49152:65535/tcp && \
|
||||
ufw allow 49152:65535/udp
|
||||
# Pour Ubuntu
|
||||
sudo ufw allow 3478 && \
|
||||
sudo ufw allow 443 && \
|
||||
sudo ufw allow 5349 && \
|
||||
sudo ufw allow 49152:65535/tcp && \
|
||||
sudo ufw allow 49152:65535/udp
|
||||
|
||||
# Pour Fedora
|
||||
sudo firewall-cmd --permanent --add-port=443/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=443/udp && \
|
||||
sudo firewall-cmd --permanent --add-port=5349/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=5349/udp && \
|
||||
sudo firewall-cmd --permanent --add-port=49152:65535/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=49152:65535/udp && \
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
## Configurer l'app mobile
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# Connection switch improvements
|
||||
|
||||
## Problems
|
||||
|
||||
- State of switch is unknown to chat, making it unclear if switch is progressing or stuck.
|
||||
|
||||
- Multiple connection switches can be triggered before the first one completes, leaving unused queues.
|
||||
|
||||
## Solution
|
||||
|
||||
Propagate switch state to UI via extended `ConnectionStats`. UI will display switch status if switch is in progress, the button to initiate switch will be disabled depending on switch state.
|
||||
|
||||
### Agent backend
|
||||
|
||||
Add following fields to `connections` table:
|
||||
|
||||
```sql
|
||||
ALTER TABLE connections ADD COLUMN rcv_switch_status TEXT;
|
||||
ALTER TABLE connections ADD COLUMN snd_switch_status TEXT;
|
||||
```
|
||||
|
||||
We can use either existing `SwitchPhase` type as status, or new more detailed types separate for rcv and snd switch.
|
||||
|
||||
```haskell
|
||||
data RcvSwitchStatus
|
||||
= RSSQueuingSwch -- set in beginning of switchConnectionAsync' before queueing SWCH command
|
||||
| RSSSwchStarted -- set in beginning of switchConnection'
|
||||
| RSSQueuingQADD -- set in switchConnection' before queuing QADD
|
||||
| RSSSentQADD -- set in runSmpQueueMsgDelivery after receiving Right in response to sending QADD
|
||||
| RSSReceivedQKEY -- set on receiving QKEY, in beginning of qKeyMsg
|
||||
| RSSQueueingSecure -- set before queueing ICQSecure in qKeyMsg
|
||||
| RSSSecureStarted -- set in beginning of ICQSecure processing in runCommandProcessing
|
||||
| RSSQueueingQUSE -- set in ICQSecure processing before queueing QUSE
|
||||
| RSSMessageReceived -- set after receiving first message in the new queue (processSMPTransmission, setRcvQueuePrimary)
|
||||
| RSSQueueingDelete -- set before queuing ICQDelete in processSMPTransmission
|
||||
| RSSDeleteStarted -- set in beginning of ICQDelete processing in runCommandProcessing
|
||||
|
||||
-- after ICQDelete processing rcv_switch_status is set back to NULL, on internal errors as well
|
||||
|
||||
canStopRcvSwitch :: RcvSwitchStatus -> Bool
|
||||
canStopRcvSwitch = \case
|
||||
RSSSentQADD -> True
|
||||
_ -> False
|
||||
```
|
||||
|
||||
I don't know use for most of the statuses yet, except for debugging, and more granular control over converting switch state into UI representation. At least it seems necessary to record the very beginning of switch (async command, sync command), points of sending QADD and receiving QKEY, and to reset after ICQDelete. `SwitchPhase` type seems to be insufficient.
|
||||
|
||||
When it is in progress, repeatedly switching connection in UI is only allowed in RSSSentQADD status - to avoid race conditions with internal commands processing, and sender processing.
|
||||
|
||||
Logic of repeat switch of connection could be split into stopSwitchConnection and existing start switch command on client level (basically chat automating two buttons for user).
|
||||
|
||||
```haskell
|
||||
stopSwitchConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m ConnectionStats
|
||||
stopSwitchConnection c = do
|
||||
-- in transaction:
|
||||
-- if rcv_switch_status is RSSSentQADD:
|
||||
-- - deleteConnRcvQueue
|
||||
-- - reset rcv_switch_status to NULL
|
||||
-- else: throw error
|
||||
-- deleteQueue on server
|
||||
```
|
||||
|
||||
Repeat switch would send repeat QADD to sender, sender should delete snd queue that was previously set to replace existing queue (check in qAddMsg).
|
||||
|
||||
Snd switch statuses:
|
||||
|
||||
```haskell
|
||||
data SndSwitchStatus
|
||||
= SSSReceivedQADD -- set on receiving QADD, in beginning of qAddMsg
|
||||
| SSSQueueingQKEY -- set in qAddMsg before queuing QKEY
|
||||
| SSSReceivedQUSE -- set on receiving QUSE, in beginning of qUseMsg
|
||||
| SSSQueueingQTEST -- set in qUseMsg before queuing QTEST
|
||||
| SSSSentQTEST -- set in runSmpQueueMsgDelivery after receiving Right in response to sending QTEST
|
||||
|
||||
-- after processing AM_QTEST_ in runSmpQueueMsgDelivery, snd_switch_status is set back to NULL
|
||||
```
|
||||
|
||||
In case of send it is enough to know that "snd switch is in progress", as sender cannot do any actions with recipient's switch, so status granularity is not necessary, but it can be useful for debugging and UI representation.
|
||||
|
||||
---
|
||||
Problem:
|
||||
If permanent errors in the new queue prevent switch from completing, it will be stuck forever and repeat switch will not be allowed. How to abort switch in this case while switch is in progress?
|
||||
|
||||
Solution:
|
||||
- In case of a permanent error on recipient side, delete queue (deleteQueue on server, catching errors; deleteConnRcvQueue in database), reset rcv_switch_status to NULL. However, if rcv_switch_status is one of RSSMessageReceived, RSSQueueingDelete, RSSDeleteStarted, it means that sender has already deleted the original queue and will not be able to revert, so the recipient has to complete switch regardless of permanent errors.
|
||||
- In case of a permanent error on sender side (e.g. AUTH error when trying to send QTEST), recipient has no way of knowing switch will never complete - one option is for sender to send a new message "QERR SMPQueueInfo" ( / QFAIL) in the original queue and delete queue from database; after receiving QERR recipient tries to delete queue on server, deletes queue in database, resets rcv_switch_status to NULL.
|
||||
|
||||
A new SPFailed SwitchPhase might be required for notifying client, so that switch failure is visible in client as a new chat item.
|
||||
|
||||
In case this new QERR message would have to be added anyway - why allow stopping/re-triggering switch at all? Sender agent could send QERR on permanent error when trying to send QKEY as well. This would allow to reduce number or required statuses, and remove the need to introduce stopSwitchConnection api / stop logic in agent.
|
||||
---
|
||||
|
||||
Type to communicate switch status to chat and UI:
|
||||
|
||||
```haskell
|
||||
-- rename:
|
||||
-- ConnectionStats -> ConnectionInfo,
|
||||
-- getConnectionServers -> getConnectionInfo
|
||||
|
||||
data ConnectionInfo = ConnectionInfo
|
||||
{ rcvServers :: [SMPServer],
|
||||
sndServers :: [SMPServer],
|
||||
rcvSwitchStatus :: Maybe RcvSwitchStatus,
|
||||
sndSwitchStatus :: Maybe SndSwitchStatus
|
||||
}
|
||||
```
|
||||
|
||||
Existing connections:
|
||||
|
||||
- Should switch be allowed for existing connections with switch in progress?
|
||||
- If switch should not be allowed, how to distinguish whether switch is in progress - by connection having multiple queues?
|
||||
- If yes, how to account for connections with extra queues from unfinished switches?
|
||||
|
||||
It's the easiest to allow switch for existing connections with switch already in progress and/or multiple switching queues.It is currently allowed, so it's not breaking anything new.
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name: simplex-chat
|
||||
version: 5.1.2.0
|
||||
version: 5.1.3.0
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
|
||||
@@ -2,20 +2,34 @@
|
||||
# Safety measures
|
||||
set -eu
|
||||
|
||||
repo="https://github.com/simplex-chat/simplex-chat"
|
||||
|
||||
u="$USER"
|
||||
tmp=$(mktemp -d -t)
|
||||
tmp="$(mktemp -d -t)"
|
||||
folder="$tmp/simplex-chat"
|
||||
|
||||
nix_ver="nix-2.15.1"
|
||||
nix_url="https://releases.nixos.org/nix/$nix_ver/install"
|
||||
nix_hash="67aa37f0115195d8ddf32b5d6f471f1e60ecca0fdb3e98bcf54bc147c3078640"
|
||||
nix_config="sandbox = true
|
||||
max-jobs = auto
|
||||
experimental-features = nix-command flakes"
|
||||
|
||||
commands="nix git curl gradle zip unzip zipalign"
|
||||
arches="${ARCHES:-aarch64 armv7a}"
|
||||
|
||||
arch_map() {
|
||||
case $1 in
|
||||
aarch64) android_arch="arm64-v8a" ;;
|
||||
armv7a) android_arch="armeabi-v7a" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
nix_install() {
|
||||
# Pre-setup nix
|
||||
[ ! -d /nix ] && sudo sh -c "mkdir -p /nix && chown -R $u /nix"
|
||||
|
||||
# Install nix
|
||||
nix_ver="nix-2.14.1"
|
||||
nix_url="https://releases.nixos.org/nix/$nix_ver/install"
|
||||
nix_hash="565974057264f0536f600c68d59395927cd73e9fc5a60f33c1906e8f7bc33fcf"
|
||||
|
||||
curl -sSf "$nix_url" -o "$tmp/nix-install"
|
||||
printf "%s %s" "$nix_hash" "$tmp/nix-install" | sha256sum -c
|
||||
chmod +x "$tmp/nix-install" && "$tmp/nix-install" --no-daemon
|
||||
@@ -24,21 +38,17 @@ nix_install() {
|
||||
}
|
||||
|
||||
nix_setup() {
|
||||
printf "sandbox = true\nmax-jobs = auto\nexperimental-features = nix-command flakes\n" > "$tmp/nix.conf"
|
||||
printf "%s" "$nix_config" > "$tmp/nix.conf"
|
||||
export NIX_CONF_DIR="$tmp/"
|
||||
}
|
||||
|
||||
git_setup() {
|
||||
[ "$folder" != "." ] && {
|
||||
git clone --depth=1 https://github.com/simplex-chat/simplex-chat "$folder"
|
||||
git clone --depth=1 "$repo" "$folder"
|
||||
}
|
||||
|
||||
# Switch to nix-android branch
|
||||
git -C "$folder" checkout "$commit"
|
||||
|
||||
# Create missing folders
|
||||
mkdir -p "$folder/apps/android/app/src/main/cpp/libs/arm64-v8a"
|
||||
mkdir -p "$folder/apps/android/app/src/main/cpp/libs/armeabi-v7a"
|
||||
}
|
||||
|
||||
checks() {
|
||||
@@ -70,38 +80,53 @@ checks() {
|
||||
}
|
||||
|
||||
build() {
|
||||
# Build simplex lib
|
||||
nix build "$folder#hydraJobs.aarch64-android:lib:simplex-chat.x86_64-linux"
|
||||
unzip -o "$PWD/result/pkg-aarch64-android-libsimplex.zip" -d "$folder/apps/android/app/src/main/cpp/libs/arm64-v8a"
|
||||
|
||||
nix build "$folder#hydraJobs.armv7a-android:lib:simplex-chat.x86_64-linux"
|
||||
unzip -o "$PWD/result/pkg-armv7a-android-libsimplex.zip" -d "$folder/apps/android/app/src/main/cpp/libs/armeabi-v7a"
|
||||
|
||||
# Build android suppprt lib
|
||||
nix build "$folder#hydraJobs.aarch64-android:lib:support.x86_64-linux"
|
||||
unzip -o "$PWD/result/pkg-aarch64-android-libsupport.zip" -d "$folder/apps/android/app/src/main/cpp/libs/arm64-v8a"
|
||||
|
||||
nix build "$folder#hydraJobs.armv7a-android:lib:support.x86_64-linux"
|
||||
unzip -o "$PWD/result/pkg-armv7a-android-libsupport.zip" -d "$folder/apps/android/app/src/main/cpp/libs/armeabi-v7a"
|
||||
|
||||
# Build preparations
|
||||
sed -i.bak 's/${extract_native_libs}/true/' "$folder/apps/android/app/src/main/AndroidManifest.xml"
|
||||
sed -i.bak '/android {/a lint {abortOnError false}' "$folder/apps/android/app/build.gradle"
|
||||
|
||||
gradle -p "$folder/apps/android/" clean build assembleRelease
|
||||
for arch in $arches; do
|
||||
android_simplex_lib="${folder}#hydraJobs.${arch}-android:lib:simplex-chat.x86_64-linux"
|
||||
android_support_lib="${folder}#hydraJobs.${arch}-android:lib:support.x86_64-linux"
|
||||
android_simplex_lib_output="${PWD}/result/pkg-${arch}-android-libsimplex.zip"
|
||||
android_support_lib_output="${PWD}/result/pkg-${arch}-android-libsupport.zip"
|
||||
|
||||
arch_map "$arch"
|
||||
|
||||
mkdir -p "$tmp/android-aarch64"
|
||||
unzip -oqd "$tmp/android-aarch64/" "$folder/apps/android/app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk"
|
||||
(cd "$tmp/android-aarch64" && zip -rq5 "$tmp/simplex-chat-aarch64.apk" . && zip -rq0 "$tmp/simplex-chat-aarch64.apk" resources.arsc res)
|
||||
zipalign -p -f 4 "$tmp/simplex-chat-aarch64.apk" "$PWD/simplex-chat-aarch64.apk"
|
||||
|
||||
mkdir -p "$tmp/android-armv7"
|
||||
unzip -oqd "$tmp/android-armv7/" "$folder/apps/android/app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned.apk"
|
||||
(cd "$tmp/android-armv7" && zip -rq5 "$tmp/simplex-chat-armv7.apk" . && zip -rq0 "$tmp/simplex-chat-armv7.apk" resources.arsc res)
|
||||
zipalign -p -f 4 "$tmp/simplex-chat-armv7.apk" "$PWD/simplex-chat-armv7.apk"
|
||||
android_tmp_folder="${tmp}/android-${arch}"
|
||||
android_apk_output="${folder}/apps/android/app/build/outputs/apk/release/app-${android_arch}-release-unsigned.apk"
|
||||
android_apk_output_final="simplex-chat-${android_arch}.apk"
|
||||
libs_folder="$folder/apps/android/app/src/main/cpp/libs"
|
||||
|
||||
# Create missing folders
|
||||
mkdir -p "$libs_folder/$android_arch"
|
||||
|
||||
nix build "$android_simplex_lib"
|
||||
unzip -o "$android_simplex_lib_output" -d "$libs_folder/$android_arch"
|
||||
|
||||
nix build "$android_support_lib"
|
||||
unzip -o "$android_support_lib_output" -d "$libs_folder/$android_arch"
|
||||
|
||||
# Build only one arch
|
||||
sed -i.bak "s/include '.*/include '${android_arch}'/" "$folder/apps/android/app/build.gradle"
|
||||
gradle -p "$folder/apps/android/" clean assembleRelease
|
||||
|
||||
mkdir -p "$android_tmp_folder"
|
||||
unzip -oqd "$android_tmp_folder" "$android_apk_output"
|
||||
|
||||
(
|
||||
cd "$android_tmp_folder" && \
|
||||
zip -rq5 "$tmp/$android_apk_output_final" . && \
|
||||
zip -rq0 "$tmp/$android_apk_output_final" resources.arsc res
|
||||
)
|
||||
|
||||
zipalign -p -f 4 "$tmp/$android_apk_output_final" "$PWD/$android_apk_output_final"
|
||||
|
||||
rm -rf "$libs_folder/$android_arch"
|
||||
done
|
||||
}
|
||||
|
||||
final() {
|
||||
printf "Simplex-chat was successfully compiled: %s/simplex-chat.apk\nDelete nix and gradle caches with 'rm -rf /nix && rm \$HOME/.nix* && \$HOME/.gradle/caches' in case if no longer needed.\n" "$PWD"
|
||||
printf 'Simplex-chat was successfully compiled: %s/simplex-chat-*.apk\nDelete nix and gradle caches with "rm -rf /nix && rm $HOME/.nix* && $HOME/.gradle/caches" in case if no longer needed.\n' "$PWD"
|
||||
}
|
||||
|
||||
main() {
|
||||
|
||||
+4
-1
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 5.1.2.0
|
||||
version: 5.1.3.0
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
@@ -99,6 +99,8 @@ library
|
||||
Simplex.Chat.Migrations.M20230511_reactions
|
||||
Simplex.Chat.Migrations.M20230519_item_deleted_ts
|
||||
Simplex.Chat.Migrations.M20230526_indexes
|
||||
Simplex.Chat.Migrations.M20230529_indexes
|
||||
Simplex.Chat.Migrations.M20230608_deleted_contacts
|
||||
Simplex.Chat.Mobile
|
||||
Simplex.Chat.Mobile.WebRTC
|
||||
Simplex.Chat.Options
|
||||
@@ -373,6 +375,7 @@ test-suite simplex-chat-test
|
||||
MobileTests
|
||||
ProtocolTests
|
||||
SchemaDump
|
||||
ViewTests
|
||||
WebRTCTests
|
||||
Paths_simplex_chat
|
||||
hs-source-dirs:
|
||||
|
||||
+110
-87
@@ -28,7 +28,7 @@ import Data.Bifunctor (bimap, first)
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Char (isSpace)
|
||||
import Data.Char (isSpace, toLower)
|
||||
import Data.Constraint (Dict (..))
|
||||
import Data.Either (fromRight, rights)
|
||||
import Data.Fixed (div')
|
||||
@@ -46,7 +46,6 @@ import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Time (NominalDiffTime, addUTCTime, defaultTimeLocale, formatTime)
|
||||
import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDay, nominalDiffTimeToSeconds)
|
||||
import Data.Time.Clock.System (SystemTime, systemToUTCTime)
|
||||
import Data.Time.LocalTime (getCurrentTimeZone, getZonedTime)
|
||||
import Data.Word (Word32)
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Simplex.Chat.Archive
|
||||
@@ -122,22 +121,26 @@ defaultChatConfig =
|
||||
testView = False,
|
||||
initialCleanupManagerDelay = 30 * 1000000, -- 30 seconds
|
||||
cleanupManagerInterval = 30 * 60, -- 30 minutes
|
||||
cleanupManagerStepDelay = 3 * 1000000, -- 3 seconds
|
||||
ciExpirationInterval = 30 * 60 * 1000000 -- 30 minutes
|
||||
}
|
||||
|
||||
_defaultSMPServers :: NonEmpty SMPServerWithAuth
|
||||
_defaultSMPServers =
|
||||
L.fromList
|
||||
[ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion",
|
||||
"smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion",
|
||||
"smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion"
|
||||
[ "smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion",
|
||||
"smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion",
|
||||
"smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion"
|
||||
]
|
||||
|
||||
_defaultNtfServers :: [NtfServer]
|
||||
_defaultNtfServers = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,ntg7jdjy2i3qbib3sykiho3enekwiaqg3icctliqhtqcg6jmoh6cxiad.onion"]
|
||||
|
||||
maxImageSize :: Integer
|
||||
maxImageSize = 236700
|
||||
maxImageSize = 261120 * 2 -- auto-receive on mobiles
|
||||
|
||||
imageExtensions :: [String]
|
||||
imageExtensions = [".jpg", ".jpeg", ".png", ".gif"]
|
||||
|
||||
maxMsgReactions :: Int
|
||||
maxMsgReactions = 3
|
||||
@@ -222,25 +225,23 @@ cfgServers = \case
|
||||
startChatController :: forall m. ChatMonad' m => Bool -> Bool -> Bool -> m (Async ())
|
||||
startChatController subConns enableExpireCIs startXFTPWorkers = do
|
||||
asks smpAgent >>= resumeAgentClient
|
||||
users <- timeItToView "startChatController, getUsers" $ fromRight [] <$> runExceptT (withStore' getUsers)
|
||||
timeItToView "startChatController, restoreCalls" $ restoreCalls
|
||||
users <- fromRight [] <$> runExceptT (withStoreCtx' (Just "startChatController, getUsers") getUsers)
|
||||
restoreCalls
|
||||
s <- asks agentAsync
|
||||
readTVarIO s >>= maybe (start s users) (pure . fst)
|
||||
where
|
||||
start s users = do
|
||||
a1 <- timeItToView "startChatController, a1" $ async $ race_ notificationSubscriber agentSubscriber
|
||||
a1 <- async $ race_ notificationSubscriber agentSubscriber
|
||||
a2 <-
|
||||
timeItToView "startChatController, a2" $
|
||||
if subConns
|
||||
then Just <$> async (subscribeUsers users)
|
||||
else pure Nothing
|
||||
if subConns
|
||||
then Just <$> async (subscribeUsers users)
|
||||
else pure Nothing
|
||||
atomically . writeTVar s $ Just (a1, a2)
|
||||
when startXFTPWorkers $ do
|
||||
timeItToView "startChatController, startXFTP" $ startXFTP
|
||||
timeItToView "startChatController, forkIO startFilesToReceive" $ void $ forkIO $ startFilesToReceive users
|
||||
timeItToView "startChatController, startCleanupManager" $ startCleanupManager
|
||||
when enableExpireCIs $
|
||||
timeItToView "startChatController, startExpireCIs" $ startExpireCIs users
|
||||
startXFTP
|
||||
void $ forkIO $ startFilesToReceive users
|
||||
startCleanupManager
|
||||
when enableExpireCIs $ startExpireCIs users
|
||||
pure a1
|
||||
startXFTP = do
|
||||
tmp <- readTVarIO =<< asks tempDirectory
|
||||
@@ -256,7 +257,7 @@ startChatController subConns enableExpireCIs startXFTPWorkers = do
|
||||
_ -> pure ()
|
||||
startExpireCIs users =
|
||||
forM_ users $ \user -> do
|
||||
ttl <- fromRight Nothing <$> runExceptT (withStore' (`getChatItemTTL` user))
|
||||
ttl <- fromRight Nothing <$> runExceptT (withStoreCtx' (Just "startExpireCIs, getChatItemTTL") (`getChatItemTTL` user))
|
||||
forM_ ttl $ \_ -> do
|
||||
startExpireCIThread user
|
||||
setExpireCIFlag user True
|
||||
@@ -281,14 +282,14 @@ startFilesToReceive users = do
|
||||
|
||||
startReceiveUserFiles :: forall m. ChatMonad m => User -> m ()
|
||||
startReceiveUserFiles user = do
|
||||
filesToReceive <- withStore' (`getRcvFilesToReceive` user)
|
||||
filesToReceive <- withStoreCtx' (Just "startReceiveUserFiles, getRcvFilesToReceive") (`getRcvFilesToReceive` user)
|
||||
forM_ filesToReceive $ \ft ->
|
||||
flip catchError (toView . CRChatError (Just user)) $
|
||||
toView =<< receiveFile' user ft Nothing Nothing
|
||||
|
||||
restoreCalls :: ChatMonad' m => m ()
|
||||
restoreCalls = do
|
||||
savedCalls <- fromRight [] <$> runExceptT (withStore' $ \db -> getCalls db)
|
||||
savedCalls <- fromRight [] <$> runExceptT (withStoreCtx' (Just "restoreCalls, getCalls") $ \db -> getCalls db)
|
||||
let callsMap = M.fromList $ map (\call@Call {contactId} -> (contactId, call)) savedCalls
|
||||
calls <- asks currentCalls
|
||||
atomically $ writeTVar calls callsMap
|
||||
@@ -354,9 +355,9 @@ processChatCommand = \case
|
||||
asks currentUser >>= readTVarIO >>= \case
|
||||
Nothing -> throwChatError CENoActiveUser
|
||||
Just user -> do
|
||||
smpServers <- withStore' (`getProtocolServers` user)
|
||||
servers <- withStore' (`getProtocolServers` user)
|
||||
cfg <- asks config
|
||||
pure (activeAgentServers cfg protocol smpServers, smpServers)
|
||||
pure (activeAgentServers cfg protocol servers, servers)
|
||||
| otherwise = do
|
||||
defServers <- asks $ defaultServers . config
|
||||
pure (cfgServers protocol defServers, [])
|
||||
@@ -365,11 +366,11 @@ processChatCommand = \case
|
||||
withStore $ \db -> overwriteProtocolServers db user servers
|
||||
coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day)
|
||||
day = 86400
|
||||
ListUsers -> CRUsersList <$> withStore' getUsersInfo
|
||||
ListUsers -> CRUsersList <$> withStoreCtx' (Just "ListUsers, getUsersInfo") getUsersInfo
|
||||
APISetActiveUser userId' viewPwd_ -> withUser $ \user -> do
|
||||
user' <- privateGetUser userId'
|
||||
validateUserPassword user user' viewPwd_
|
||||
withStore' $ \db -> setActiveUser db userId'
|
||||
withStoreCtx' (Just "APISetActiveUser, setActiveUser") $ \db -> setActiveUser db userId'
|
||||
setActive ActiveNone
|
||||
let user'' = user' {activeUser = True}
|
||||
asks currentUser >>= atomically . (`writeTVar` Just user'')
|
||||
@@ -423,14 +424,14 @@ processChatCommand = \case
|
||||
APIActivateChat -> withUser $ \_ -> do
|
||||
restoreCalls
|
||||
withAgent foregroundAgent
|
||||
withStore' getUsers >>= void . forkIO . startFilesToReceive
|
||||
withStoreCtx' (Just "APIActivateChat, getUsers") getUsers >>= void . forkIO . startFilesToReceive
|
||||
setAllExpireCIFlags True
|
||||
ok_
|
||||
APISuspendChat t -> do
|
||||
setAllExpireCIFlags False
|
||||
withAgent (`suspendAgent` t)
|
||||
ok_
|
||||
ResubscribeAllConnections -> withStore' getUsers >>= subscribeUsers >> ok_
|
||||
ResubscribeAllConnections -> withStoreCtx' (Just "ResubscribeAllConnections, getUsers") getUsers >>= subscribeUsers >> ok_
|
||||
-- has to be called before StartChat
|
||||
SetTempFolder tf -> do
|
||||
createDirectoryIfMissing True tf
|
||||
@@ -460,7 +461,7 @@ processChatCommand = \case
|
||||
ExecChatStoreSQL query -> CRSQLResult <$> withStore' (`execSQL` query)
|
||||
ExecAgentStoreSQL query -> CRSQLResult <$> withAgent (`execAgentStoreSQL` query)
|
||||
APIGetChats userId withPCC -> withUserId userId $ \user ->
|
||||
CRApiChats user <$> withStore' (\db -> getChatPreviews db user withPCC)
|
||||
CRApiChats user <$> withStoreCtx' (Just "APIGetChats, getChatPreviews") (\db -> getChatPreviews db user withPCC)
|
||||
APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of
|
||||
-- TODO optimize queries calculating ChatStats, currently they're disabled
|
||||
CTDirect -> do
|
||||
@@ -867,7 +868,7 @@ processChatCommand = \case
|
||||
Just _ -> pure []
|
||||
Nothing -> do
|
||||
conns <- withStore $ \db -> getContactConnections db userId ct
|
||||
withStore' (\db -> deleteContactWithoutGroups db user ct)
|
||||
withStore' (\db -> setContactDeleted db user ct)
|
||||
`catchError` (toView . CRChatError (Just user))
|
||||
pure $ map aConnId conns
|
||||
CTContactRequest -> pure $ chatCmdError (Just user) "not supported"
|
||||
@@ -1065,7 +1066,7 @@ processChatCommand = \case
|
||||
SetChatItemTTL newTTL_ -> withUser' $ \User {userId} -> do
|
||||
processChatCommand $ APISetChatItemTTL userId newTTL_
|
||||
APIGetChatItemTTL userId -> withUserId userId $ \user -> do
|
||||
ttl <- withStore' (`getChatItemTTL` user)
|
||||
ttl <- withStoreCtx' (Just "APIGetChatItemTTL, getChatItemTTL") (`getChatItemTTL` user)
|
||||
pure $ CRChatItemTTL user ttl
|
||||
GetChatItemTTL -> withUser' $ \User {userId} -> do
|
||||
processChatCommand $ APIGetChatItemTTL userId
|
||||
@@ -1222,7 +1223,7 @@ processChatCommand = \case
|
||||
DeleteMyAddress -> withUser $ \User {userId} ->
|
||||
processChatCommand $ APIDeleteMyAddress userId
|
||||
APIShowMyAddress userId -> withUserId userId $ \user ->
|
||||
CRUserContactLink user <$> withStore (`getUserAddress` user)
|
||||
CRUserContactLink user <$> withStoreCtx (Just "APIShowMyAddress, getUserAddress") (`getUserAddress` user)
|
||||
ShowMyAddress -> withUser $ \User {userId} ->
|
||||
processChatCommand $ APIShowMyAddress userId
|
||||
APISetProfileAddress userId False -> withUserId userId $ \user@User {profile = p} -> do
|
||||
@@ -1259,7 +1260,7 @@ processChatCommand = \case
|
||||
saveSndChatItem user (CDDirectSnd ct) sndMsg (CISndMsgContent mc)
|
||||
)
|
||||
`catchError` (toView . CRChatError (Just user))
|
||||
CRBroadcastSent user mc (length cts) <$> liftIO getZonedTime
|
||||
CRBroadcastSent user mc (length cts) <$> liftIO getCurrentTime
|
||||
SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do
|
||||
contactId <- withStore $ \db -> getContactIdByName db user cName
|
||||
quotedItemId <- withStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg
|
||||
@@ -1473,6 +1474,7 @@ processChatCommand = \case
|
||||
LastMessages (Just chatName) count search -> withUser $ \user -> do
|
||||
chatRef <- getChatRef user chatName
|
||||
chatResp <- processChatCommand $ APIGetChat chatRef (CPLast count) search
|
||||
setActive $ chatActiveTo chatName
|
||||
pure $ CRChatItems user (aChatItems . chat $ chatResp)
|
||||
LastMessages Nothing count search -> withUser $ \user -> do
|
||||
chatItems <- withStore $ \db -> getAllChatItems db user (CPLast count) search
|
||||
@@ -1504,7 +1506,7 @@ processChatCommand = \case
|
||||
SendImage chatName f -> withUser $ \user -> do
|
||||
chatRef <- getChatRef user chatName
|
||||
filePath <- toFSFilePath f
|
||||
unless (".jpg" `isSuffixOf` f || ".jpeg" `isSuffixOf` f) $ throwChatError CEFileImageType {filePath}
|
||||
unless (any ((`isSuffixOf` map toLower f)) imageExtensions) $ throwChatError CEFileImageType {filePath}
|
||||
fileSize <- getFileSize filePath
|
||||
unless (fileSize <= maxImageSize) $ throwChatError CEFileImageSize {filePath}
|
||||
-- TODO include file description for preview
|
||||
@@ -1938,12 +1940,14 @@ startExpireCIThread user@User {userId} = do
|
||||
_ -> pure ()
|
||||
where
|
||||
runExpireCIs = do
|
||||
delay <- asks (initialCleanupManagerDelay . config)
|
||||
liftIO $ threadDelay' delay
|
||||
interval <- asks $ ciExpirationInterval . config
|
||||
forever $ do
|
||||
flip catchError (toView . CRChatError (Just user)) $ do
|
||||
expireFlags <- asks expireCIFlags
|
||||
atomically $ TM.lookup userId expireFlags >>= \b -> unless (b == Just True) retry
|
||||
ttl <- withStore' (`getChatItemTTL` user)
|
||||
ttl <- withStoreCtx' (Just "startExpireCIThread, getChatItemTTL") (`getChatItemTTL` user)
|
||||
forM_ ttl $ \t -> expireChatItems user t False
|
||||
liftIO $ threadDelay' interval
|
||||
|
||||
@@ -2067,11 +2071,11 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
|
||||
(Nothing, Just connReq) -> do
|
||||
connIds <- joinAgentConnectionAsync user True connReq . directMessage $ XFileAcpt fName
|
||||
filePath <- getRcvFilePath fileId filePath_ fName True
|
||||
withStore $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath
|
||||
withStoreCtx (Just "acceptFileReceive, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath
|
||||
-- XFTP
|
||||
(Just _xftpRcvFile, _) -> do
|
||||
filePath <- getRcvFilePath fileId filePath_ fName False
|
||||
(ci, rfd) <- withStore $ \db -> do
|
||||
(ci, rfd) <- withStoreCtx (Just "acceptFileReceive, xftpAcceptRcvFT ...") $ \db -> do
|
||||
-- marking file as accepted and reading description in the same transaction
|
||||
-- to prevent race condition with appending description
|
||||
ci <- xftpAcceptRcvFT db user fileId filePath
|
||||
@@ -2081,13 +2085,13 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
|
||||
pure ci
|
||||
-- group & direct file protocol
|
||||
_ -> do
|
||||
chatRef <- withStore $ \db -> getChatRefByFileId db user fileId
|
||||
chatRef <- withStoreCtx (Just "acceptFileReceive, getChatRefByFileId") $ \db -> getChatRefByFileId db user fileId
|
||||
case (chatRef, grpMemberId) of
|
||||
(ChatRef CTDirect contactId, Nothing) -> do
|
||||
ct <- withStore $ \db -> getContact db user contactId
|
||||
ct <- withStoreCtx (Just "acceptFileReceive, getContact") $ \db -> getContact db user contactId
|
||||
acceptFile CFCreateConnFileInvDirect $ \msg -> void $ sendDirectContactMessage ct msg
|
||||
(ChatRef CTGroup groupId, Just memId) -> do
|
||||
GroupMember {activeConn} <- withStore $ \db -> getGroupMember db user groupId memId
|
||||
GroupMember {activeConn} <- withStoreCtx (Just "acceptFileReceive, getGroupMember") $ \db -> getGroupMember db user groupId memId
|
||||
case activeConn of
|
||||
Just conn -> do
|
||||
acceptFile CFCreateConnFileInvGroup $ \msg -> void $ sendDirectMessage conn msg $ GroupId groupId
|
||||
@@ -2101,7 +2105,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
|
||||
if
|
||||
| inline -> do
|
||||
-- accepting inline
|
||||
ci <- withStore $ \db -> acceptRcvInlineFT db user fileId filePath
|
||||
ci <- withStoreCtx (Just "acceptFile, acceptRcvInlineFT") $ \db -> acceptRcvInlineFT db user fileId filePath
|
||||
sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId
|
||||
send $ XFileAcptInv sharedMsgId Nothing fName
|
||||
pure ci
|
||||
@@ -2109,7 +2113,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
|
||||
| otherwise -> do
|
||||
-- accepting via a new connection
|
||||
connIds <- createAgentConnectionAsync user cmdFunction True SCMInvitation
|
||||
withStore $ \db -> acceptRcvFileTransfer db user fileId connIds ConnNew filePath
|
||||
withStoreCtx (Just "acceptFile, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnNew filePath
|
||||
receiveInline :: m Bool
|
||||
receiveInline = do
|
||||
ChatConfig {fileChunkSize, inlineFiles = InlineFilesConfig {receiveChunks, offerChunks}} <- asks config
|
||||
@@ -2126,11 +2130,11 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete}
|
||||
rd <- parseFileDescription fileDescrText
|
||||
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd
|
||||
startReceivingFile user fileId
|
||||
withStore' $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId)
|
||||
withStoreCtx' (Just "receiveViaCompleteFD, updateRcvFileAgentId") $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId)
|
||||
|
||||
startReceivingFile :: ChatMonad m => User -> FileTransferId -> m ()
|
||||
startReceivingFile user fileId = do
|
||||
ci <- withStore $ \db -> do
|
||||
ci <- withStoreCtx (Just "startReceivingFile, updateRcvFileStatus ...") $ \db -> do
|
||||
liftIO $ updateRcvFileStatus db fileId FSConnected
|
||||
liftIO $ updateCIFileStatus db user fileId $ CIFSRcvTransfer 0 1
|
||||
getChatItemByFileId db user fileId
|
||||
@@ -2239,7 +2243,7 @@ agentSubscriber = do
|
||||
type AgentBatchSubscribe m = AgentClient -> [ConnId] -> ExceptT AgentErrorType m (Map ConnId (Either AgentErrorType ()))
|
||||
|
||||
subscribeUserConnections :: forall m. ChatMonad m => AgentBatchSubscribe m -> User -> m ()
|
||||
subscribeUserConnections agentBatchSubscribe user = do
|
||||
subscribeUserConnections agentBatchSubscribe user@User {userId} = do
|
||||
-- get user connections
|
||||
ce <- asks $ subscriptionEvents . config
|
||||
(ctConns, cts) <- getContactConns
|
||||
@@ -2260,32 +2264,32 @@ subscribeUserConnections agentBatchSubscribe user = do
|
||||
where
|
||||
getContactConns :: m ([ConnId], Map ConnId Contact)
|
||||
getContactConns = do
|
||||
cts <- withStore_ getUserContacts
|
||||
cts <- withStore_ ("subscribeUserConnections " <> show userId <> ", getUserContacts") getUserContacts
|
||||
let connIds = map contactConnId cts
|
||||
pure (connIds, M.fromList $ zip connIds cts)
|
||||
getUserContactLinkConns :: m ([ConnId], Map ConnId UserContact)
|
||||
getUserContactLinkConns = do
|
||||
(cs, ucs) <- unzip <$> withStore_ getUserContactLinks
|
||||
(cs, ucs) <- unzip <$> withStore_ ("subscribeUserConnections " <> show userId <> ", getUserContactLinks") getUserContactLinks
|
||||
let connIds = map aConnId cs
|
||||
pure (connIds, M.fromList $ zip connIds ucs)
|
||||
getGroupMemberConns :: m ([Group], [ConnId], Map ConnId GroupMember)
|
||||
getGroupMemberConns = do
|
||||
gs <- withStore_ getUserGroups
|
||||
gs <- withStore_ ("subscribeUserConnections " <> show userId <> ", getUserGroups") getUserGroups
|
||||
let mPairs = concatMap (\(Group _ ms) -> mapMaybe (\m -> (,m) <$> memberConnId m) ms) gs
|
||||
pure (gs, map fst mPairs, M.fromList mPairs)
|
||||
getSndFileTransferConns :: m ([ConnId], Map ConnId SndFileTransfer)
|
||||
getSndFileTransferConns = do
|
||||
sfts <- withStore_ getLiveSndFileTransfers
|
||||
sfts <- withStore_ ("subscribeUserConnections " <> show userId <> ", getLiveSndFileTransfers") getLiveSndFileTransfers
|
||||
let connIds = map sndFileTransferConnId sfts
|
||||
pure (connIds, M.fromList $ zip connIds sfts)
|
||||
getRcvFileTransferConns :: m ([ConnId], Map ConnId RcvFileTransfer)
|
||||
getRcvFileTransferConns = do
|
||||
rfts <- withStore_ getLiveRcvFileTransfers
|
||||
rfts <- withStore_ ("subscribeUserConnections " <> show userId <> ", getLiveRcvFileTransfers") getLiveRcvFileTransfers
|
||||
let rftPairs = mapMaybe (\ft -> (,ft) <$> liveRcvFileTransferConnId ft) rfts
|
||||
pure (map fst rftPairs, M.fromList rftPairs)
|
||||
getPendingContactConns :: m ([ConnId], Map ConnId PendingContactConnection)
|
||||
getPendingContactConns = do
|
||||
pcs <- withStore_ getPendingContactConnections
|
||||
pcs <- withStore_ ("subscribeUserConnections " <> show userId <> ", getPendingContactConnections") getPendingContactConnections
|
||||
let connIds = map aConnId' pcs
|
||||
pure (connIds, M.fromList $ zip connIds pcs)
|
||||
contactSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId Contact -> Bool -> m ()
|
||||
@@ -2336,8 +2340,8 @@ subscribeUserConnections agentBatchSubscribe user = do
|
||||
rcvFileSubsToView rs = mapM_ (toView . uncurry (CRRcvFileSubError user)) . filterErrors . resultsFor rs
|
||||
pendingConnSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId PendingContactConnection -> m ()
|
||||
pendingConnSubsToView rs = toView . CRPendingSubSummary user . map (uncurry PendingSubStatus) . resultsFor rs
|
||||
withStore_ :: (DB.Connection -> User -> IO [a]) -> m [a]
|
||||
withStore_ a = withStore' (`a` user) `catchError` \e -> toView (CRChatError (Just user) e) $> []
|
||||
withStore_ :: String -> (DB.Connection -> User -> IO [a]) -> m [a]
|
||||
withStore_ ctx a = withStoreCtx' (Just ctx) (`a` user) `catchError` \e -> toView (CRChatError (Just user) e) $> []
|
||||
filterErrors :: [(a, Maybe ChatError)] -> [(a, ChatError)]
|
||||
filterErrors = mapMaybe (\(a, e_) -> (a,) <$> e_)
|
||||
resultsFor :: Map ConnId (Either AgentErrorType ()) -> Map ConnId a -> [(a, Maybe ChatError)]
|
||||
@@ -2355,35 +2359,44 @@ cleanupManager :: forall m. ChatMonad m => m ()
|
||||
cleanupManager = do
|
||||
interval <- asks (cleanupManagerInterval . config)
|
||||
runWithoutInitialDelay interval
|
||||
delay <- asks (initialCleanupManagerDelay . config)
|
||||
liftIO $ threadDelay' delay
|
||||
initialDelay <- asks (initialCleanupManagerDelay . config)
|
||||
liftIO $ threadDelay' initialDelay
|
||||
stepDelay <- asks (cleanupManagerStepDelay . config)
|
||||
forever $ do
|
||||
flip catchError (toView . CRChatError Nothing) $ do
|
||||
waitChatStarted
|
||||
users <- withStore' getUsers
|
||||
users <- withStoreCtx' (Just "cleanupManager, getUsers 1") getUsers
|
||||
let (us, us') = partition activeUser users
|
||||
forM_ us $ cleanupUser interval
|
||||
forM_ us' $ cleanupUser interval
|
||||
forM_ us $ cleanupUser interval stepDelay
|
||||
forM_ us' $ cleanupUser interval stepDelay
|
||||
cleanupMessages `catchError` (toView . CRChatError Nothing)
|
||||
liftIO $ threadDelay' $ diffToMicroseconds interval
|
||||
where
|
||||
runWithoutInitialDelay cleanupInterval = flip catchError (toView . CRChatError Nothing) $ do
|
||||
waitChatStarted
|
||||
users <- withStore' getUsers
|
||||
users <- withStoreCtx' (Just "cleanupManager, getUsers 2") getUsers
|
||||
let (us, us') = partition activeUser users
|
||||
forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchError` (toView . CRChatError (Just u))
|
||||
forM_ us' $ \u -> cleanupTimedItems cleanupInterval u `catchError` (toView . CRChatError (Just u))
|
||||
cleanupUser cleanupInterval user =
|
||||
cleanupUser cleanupInterval stepDelay user = do
|
||||
cleanupTimedItems cleanupInterval user `catchError` (toView . CRChatError (Just user))
|
||||
liftIO $ threadDelay' stepDelay
|
||||
cleanupDeletedContacts user `catchError` (toView . CRChatError (Just user))
|
||||
liftIO $ threadDelay' stepDelay
|
||||
cleanupTimedItems cleanupInterval user = do
|
||||
ts <- liftIO getCurrentTime
|
||||
let startTimedThreadCutoff = addUTCTime cleanupInterval ts
|
||||
timedItems <- withStore' $ \db -> getTimedItems db user startTimedThreadCutoff
|
||||
timedItems <- withStoreCtx' (Just "cleanupManager, getTimedItems") $ \db -> getTimedItems db user startTimedThreadCutoff
|
||||
forM_ timedItems $ \(itemRef, deleteAt) -> startTimedItemThread user itemRef deleteAt `catchError` const (pure ())
|
||||
cleanupDeletedContacts user = do
|
||||
contacts <- withStore' (`getDeletedContacts` user)
|
||||
forM_ contacts $ \ct ->
|
||||
withStore' (\db -> deleteContactWithoutGroups db user ct)
|
||||
`catchError` (toView . CRChatError (Just user))
|
||||
cleanupMessages = do
|
||||
ts <- liftIO getCurrentTime
|
||||
let cutoffTs = addUTCTime (- (30 * nominalDay)) ts
|
||||
withStore' (`deleteOldMessages` cutoffTs)
|
||||
withStoreCtx' (Just "cleanupManager, deleteOldMessages") (`deleteOldMessages` cutoffTs)
|
||||
|
||||
startProximateTimedItemThread :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m ()
|
||||
startProximateTimedItemThread user itemRef deleteAt = do
|
||||
@@ -2414,10 +2427,10 @@ deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do
|
||||
waitChatStarted
|
||||
case cType of
|
||||
CTDirect -> do
|
||||
(ct, ci) <- withStore $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId
|
||||
(ct, ci) <- withStoreCtx (Just "deleteTimedItem, getContact ...") $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId
|
||||
deleteDirectCI user ct ci True True >>= toView
|
||||
CTGroup -> do
|
||||
(gInfo, ci) <- withStore $ \db -> (,) <$> getGroupInfo db user chatId <*> getGroupChatItem db user chatId itemId
|
||||
(gInfo, ci) <- withStoreCtx (Just "deleteTimedItem, getGroupInfo ...") $ \db -> (,) <$> getGroupInfo db user chatId <*> getGroupChatItem db user chatId itemId
|
||||
deletedTs <- liftIO getCurrentTime
|
||||
deleteGroupCI user gInfo ci True True Nothing deletedTs >>= toView
|
||||
_ -> toView . CRChatError (Just user) . ChatError $ CEInternalError "bad deleteTimedItem cType"
|
||||
@@ -2435,9 +2448,9 @@ expireChatItems user@User {userId} ttl sync = do
|
||||
let expirationDate = addUTCTime (-1 * fromIntegral ttl) currentTs
|
||||
-- this is to keep group messages created during last 12 hours even if they're expired according to item_ts
|
||||
createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs
|
||||
contacts <- withStore' (`getUserContacts` user)
|
||||
contacts <- withStoreCtx' (Just "expireChatItems, getUserContacts") (`getUserContacts` user)
|
||||
loop contacts $ processContact expirationDate
|
||||
groups <- withStore' (`getUserGroupDetails` user)
|
||||
groups <- withStoreCtx' (Just "expireChatItems, getUserGroupDetails") (`getUserGroupDetails` user)
|
||||
loop groups $ processGroup expirationDate createdAtCutoff
|
||||
where
|
||||
loop :: [a] -> (a -> m ()) -> m ()
|
||||
@@ -2455,16 +2468,16 @@ expireChatItems user@User {userId} ttl sync = do
|
||||
when (expire == Just True) $ threadDelay 100000 >> a
|
||||
processContact :: UTCTime -> Contact -> m ()
|
||||
processContact expirationDate ct = do
|
||||
filesInfo <- withStore' $ \db -> getContactExpiredFileInfo db user ct expirationDate
|
||||
filesInfo <- withStoreCtx' (Just "processContact, getContactExpiredFileInfo") $ \db -> getContactExpiredFileInfo db user ct expirationDate
|
||||
deleteFilesAndConns user filesInfo
|
||||
withStore' $ \db -> deleteContactExpiredCIs db user ct expirationDate
|
||||
withStoreCtx' (Just "processContact, deleteContactExpiredCIs") $ \db -> deleteContactExpiredCIs db user ct expirationDate
|
||||
processGroup :: UTCTime -> UTCTime -> GroupInfo -> m ()
|
||||
processGroup expirationDate createdAtCutoff gInfo = do
|
||||
filesInfo <- withStore' $ \db -> getGroupExpiredFileInfo db user gInfo expirationDate createdAtCutoff
|
||||
filesInfo <- withStoreCtx' (Just "processGroup, getGroupExpiredFileInfo") $ \db -> getGroupExpiredFileInfo db user gInfo expirationDate createdAtCutoff
|
||||
deleteFilesAndConns user filesInfo
|
||||
withStore' $ \db -> deleteGroupExpiredCIs db user gInfo expirationDate createdAtCutoff
|
||||
membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db user gInfo
|
||||
forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m
|
||||
withStoreCtx' (Just "processGroup, deleteGroupExpiredCIs") $ \db -> deleteGroupExpiredCIs db user gInfo expirationDate createdAtCutoff
|
||||
membersToDelete <- withStoreCtx' (Just "processGroup, getGroupMembersForExpiration") $ \db -> getGroupMembersForExpiration db user gInfo
|
||||
forM_ membersToDelete $ \m -> withStoreCtx' (Just "processGroup, deleteGroupMember") $ \db -> deleteGroupMember db user m
|
||||
|
||||
processAgentMessage :: forall m. ChatMonad m => ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> m ()
|
||||
processAgentMessage _ connId (DEL_RCVQ srv qId err_) =
|
||||
@@ -2783,7 +2796,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
_ -> pure ()
|
||||
Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) ->
|
||||
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
|
||||
notifyMemberConnected gInfo m
|
||||
notifyMemberConnected gInfo m $ Just ct
|
||||
let connectedIncognito = contactConnIncognito ct || memberIncognito membership
|
||||
when (memberCategory m == GCPreMember) $ probeMatchingContacts ct connectedIncognito
|
||||
SENT msgId -> do
|
||||
@@ -2932,11 +2945,11 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
-- TODO notify member who forwarded introduction - question - where it is stored? There is via_contact but probably there should be via_member in group_members table
|
||||
withStore' (\db -> getViaGroupContact db user m) >>= \case
|
||||
Nothing -> do
|
||||
notifyMemberConnected gInfo m
|
||||
notifyMemberConnected gInfo m Nothing
|
||||
messageWarning "connected member does not have contact"
|
||||
Just ct@Contact {activeConn = Connection {connStatus}} ->
|
||||
when (connStatus == ConnReady) $ do
|
||||
notifyMemberConnected gInfo m
|
||||
notifyMemberConnected gInfo m $ Just ct
|
||||
let connectedIncognito = contactConnIncognito ct || memberIncognito membership
|
||||
when (memberCategory m == GCPreMember) $ probeMatchingContacts ct connectedIncognito
|
||||
MSG msgMeta _msgFlags msgBody -> do
|
||||
@@ -3274,10 +3287,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
groupDescriptionChatItem gInfo m descr =
|
||||
createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvMsgContent $ MCText descr) Nothing
|
||||
|
||||
notifyMemberConnected :: GroupInfo -> GroupMember -> m ()
|
||||
notifyMemberConnected gInfo m@GroupMember {localDisplayName = c} = do
|
||||
notifyMemberConnected :: GroupInfo -> GroupMember -> Maybe Contact -> m ()
|
||||
notifyMemberConnected gInfo m@GroupMember {localDisplayName = c} ct_ = do
|
||||
memberConnectedChatItem gInfo m
|
||||
toView $ CRConnectedToGroupMember user gInfo m
|
||||
toView $ CRConnectedToGroupMember user gInfo m ct_
|
||||
let g = groupName' gInfo
|
||||
whenGroupNtfs user gInfo $ do
|
||||
setActive $ ActiveG g
|
||||
@@ -4292,7 +4305,9 @@ throwChatError = throwError . ChatError
|
||||
|
||||
deleteMembersConnections :: ChatMonad m => User -> [GroupMember] -> m ()
|
||||
deleteMembersConnections user members = do
|
||||
let memberConns = mapMaybe (\GroupMember {activeConn} -> activeConn) members
|
||||
let memberConns =
|
||||
filter (\Connection {connStatus} -> connStatus /= ConnDeleted) $
|
||||
mapMaybe (\GroupMember {activeConn} -> activeConn) members
|
||||
deleteAgentConnectionsAsync user $ map aConnId memberConns
|
||||
forM_ memberConns $ \conn -> withStore' $ \db -> updateConnectionStatus db conn ConnDeleted
|
||||
|
||||
@@ -4416,22 +4431,21 @@ saveRcvChatItem' user cd msg sharedMsgId_ MsgMeta {broker = (_, brokerTs)} conte
|
||||
|
||||
mkChatItem :: forall c d. MsgDirectionI d => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CITimed -> Bool -> ChatItemTs -> UTCTime -> IO (ChatItem c d)
|
||||
mkChatItem cd ciId content file quotedItem sharedMsgId itemTimed live itemTs currentTs = do
|
||||
tz <- getCurrentTimeZone
|
||||
let itemText = ciContentToText content
|
||||
itemStatus = ciCreateStatus content
|
||||
meta = mkCIMeta ciId content itemText itemStatus sharedMsgId Nothing False itemTimed (justTrue live) tz currentTs itemTs currentTs currentTs
|
||||
meta = mkCIMeta ciId content itemText itemStatus sharedMsgId Nothing False itemTimed (justTrue live) currentTs itemTs currentTs currentTs
|
||||
pure ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file}
|
||||
|
||||
deleteDirectCI :: ChatMonad m => User -> Contact -> CChatItem 'CTDirect -> Bool -> Bool -> m ChatResponse
|
||||
deleteDirectCI user ct ci@(CChatItem msgDir deletedItem@ChatItem {file}) byUser timed = do
|
||||
deleteCIFile user file
|
||||
withStore' $ \db -> deleteDirectChatItem db user ct ci
|
||||
withStoreCtx' (Just "deleteDirectCI, deleteDirectChatItem") $ \db -> deleteDirectChatItem db user ct ci
|
||||
pure $ CRChatItemDeleted user (AChatItem SCTDirect msgDir (DirectChat ct) deletedItem) Nothing byUser timed
|
||||
|
||||
deleteGroupCI :: ChatMonad m => User -> GroupInfo -> CChatItem 'CTGroup -> Bool -> Bool -> Maybe GroupMember -> UTCTime -> m ChatResponse
|
||||
deleteGroupCI user gInfo ci@(CChatItem msgDir deletedItem@ChatItem {file}) byUser timed byGroupMember_ deletedTs = do
|
||||
deleteCIFile user file
|
||||
toCi <- withStore' $ \db ->
|
||||
toCi <- withStoreCtx' (Just "deleteGroupCI, deleteGroupChatItem ...") $ \db ->
|
||||
case byGroupMember_ of
|
||||
Nothing -> deleteGroupChatItem db user gInfo ci $> Nothing
|
||||
Just m -> Just <$> updateGroupChatItemModerated db user gInfo ci m deletedTs
|
||||
@@ -4715,11 +4729,20 @@ withStoreCtx' ctx_ action = withStoreCtx ctx_ $ liftIO . action
|
||||
withStoreCtx :: ChatMonad m => Maybe String -> (DB.Connection -> ExceptT StoreError IO a) -> m a
|
||||
withStoreCtx ctx_ action = do
|
||||
ChatController {chatStore} <- ask
|
||||
liftEitherError ChatErrorStore $
|
||||
withTransaction chatStore (runExceptT . action) `E.catch` handleInternal
|
||||
liftEitherError ChatErrorStore $ case ctx_ of
|
||||
Nothing -> withTransaction chatStore (runExceptT . action) `E.catch` handleInternal ""
|
||||
-- uncomment to debug store performance
|
||||
-- Just ctx -> do
|
||||
-- t1 <- liftIO getCurrentTime
|
||||
-- putStrLn $ "withStoreCtx start :: " <> show t1 <> " :: " <> ctx
|
||||
-- r <- withTransactionCtx ctx_ chatStore (runExceptT . action) `E.catch` handleInternal (" (" <> ctx <> ")")
|
||||
-- t2 <- liftIO getCurrentTime
|
||||
-- putStrLn $ "withStoreCtx end :: " <> show t2 <> " :: " <> ctx <> " :: duration=" <> show (diffToMilliseconds $ diffUTCTime t2 t1)
|
||||
-- pure r
|
||||
Just _ -> withTransaction chatStore (runExceptT . action) `E.catch` handleInternal ""
|
||||
where
|
||||
handleInternal :: E.SomeException -> IO (Either StoreError a)
|
||||
handleInternal e = pure . Left . SEInternalError $ show e <> maybe "" (\ctx -> " (" <> ctx <> ")") ctx_
|
||||
handleInternal :: String -> E.SomeException -> IO (Either StoreError a)
|
||||
handleInternal ctxStr e = pure . Left . SEInternalError $ show e <> ctxStr
|
||||
|
||||
chatCommandP :: Parser ChatCommand
|
||||
chatCommandP =
|
||||
@@ -4976,7 +4999,7 @@ chatCommandP =
|
||||
onOffP = ("on" $> True) <|> ("off" $> False)
|
||||
profileNames = (,) <$> displayName <*> fullNameP
|
||||
newUserP = do
|
||||
sameServers <- "same_smp=" *> onOffP <* A.space <|> pure False
|
||||
sameServers <- "same_servers=" *> onOffP <* A.space <|> pure False
|
||||
(cName, fullName) <- profileNames
|
||||
let profile = Just Profile {displayName = cName, fullName, image = Nothing, contactLink = Nothing, preferences = Nothing}
|
||||
pure NewUser {profile, sameServers, pastTimestamp = False}
|
||||
|
||||
@@ -32,7 +32,7 @@ import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Map.Strict (Map)
|
||||
import Data.String
|
||||
import Data.Text (Text)
|
||||
import Data.Time (NominalDiffTime, ZonedTime)
|
||||
import Data.Time (NominalDiffTime)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Data.Version (showVersion)
|
||||
import GHC.Generics (Generic)
|
||||
@@ -112,6 +112,7 @@ data ChatConfig = ChatConfig
|
||||
testView :: Bool,
|
||||
initialCleanupManagerDelay :: Int64,
|
||||
cleanupManagerInterval :: NominalDiffTime,
|
||||
cleanupManagerStepDelay :: Int64,
|
||||
ciExpirationInterval :: Int64 -- microseconds
|
||||
}
|
||||
|
||||
@@ -143,6 +144,12 @@ defaultInlineFilesConfig =
|
||||
data ActiveTo = ActiveNone | ActiveC ContactName | ActiveG GroupName
|
||||
deriving (Eq)
|
||||
|
||||
chatActiveTo :: ChatName -> ActiveTo
|
||||
chatActiveTo (ChatName cType name) = case cType of
|
||||
CTDirect -> ActiveC name
|
||||
CTGroup -> ActiveG name
|
||||
_ -> ActiveNone
|
||||
|
||||
data ChatDatabase = ChatDatabase {chatStore :: SQLiteStore, agentStore :: SQLiteStore}
|
||||
|
||||
data ChatController = ChatController
|
||||
@@ -407,7 +414,7 @@ data ChatResponse
|
||||
| CRChatItemReaction {user :: User, added :: Bool, reaction :: ACIReaction}
|
||||
| CRChatItemDeleted {user :: User, deletedChatItem :: AChatItem, toChatItem :: Maybe AChatItem, byUser :: Bool, timed :: Bool}
|
||||
| CRChatItemDeletedNotFound {user :: User, contact :: Contact, sharedMsgId :: SharedMsgId}
|
||||
| CRBroadcastSent User MsgContent Int ZonedTime
|
||||
| CRBroadcastSent User MsgContent Int UTCTime
|
||||
| CRMsgIntegrityError {user :: User, msgError :: MsgErrorType}
|
||||
| CRCmdAccepted {corr :: CorrId}
|
||||
| CRCmdOk {user_ :: Maybe User}
|
||||
@@ -485,7 +492,7 @@ data ChatResponse
|
||||
| CRJoinedGroupMemberConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember}
|
||||
| CRMemberRole {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole}
|
||||
| CRMemberRoleUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole}
|
||||
| CRConnectedToGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRConnectedToGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember, memberContact :: Maybe Contact}
|
||||
| CRDeletedMember {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember}
|
||||
| CRDeletedMemberUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRLeftMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
|
||||
|
||||
@@ -26,7 +26,6 @@ import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||
import Data.Time.Clock (UTCTime, diffUTCTime, nominalDay)
|
||||
import Data.Time.LocalTime (TimeZone, ZonedTime, utcToZonedTime)
|
||||
import Data.Type.Equality
|
||||
import Data.Typeable (Typeable)
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
@@ -341,19 +340,17 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta
|
||||
itemTimed :: Maybe CITimed,
|
||||
itemLive :: Maybe Bool,
|
||||
editable :: Bool,
|
||||
localItemTs :: ZonedTime,
|
||||
createdAt :: UTCTime,
|
||||
updatedAt :: UTCTime
|
||||
}
|
||||
deriving (Show, Generic)
|
||||
|
||||
mkCIMeta :: ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe SharedMsgId -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> TimeZone -> UTCTime -> ChatItemTs -> UTCTime -> UTCTime -> CIMeta c d
|
||||
mkCIMeta itemId itemContent itemText itemStatus itemSharedMsgId itemDeleted itemEdited itemTimed itemLive tz currentTs itemTs createdAt updatedAt =
|
||||
let localItemTs = utcToZonedTime tz itemTs
|
||||
editable = case itemContent of
|
||||
mkCIMeta :: ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe SharedMsgId -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> UTCTime -> ChatItemTs -> UTCTime -> UTCTime -> CIMeta c d
|
||||
mkCIMeta itemId itemContent itemText itemStatus itemSharedMsgId itemDeleted itemEdited itemTimed itemLive currentTs itemTs createdAt updatedAt =
|
||||
let editable = case itemContent of
|
||||
CISndMsgContent _ -> diffUTCTime currentTs itemTs < nominalDay && isNothing itemDeleted
|
||||
_ -> False
|
||||
in CIMeta {itemId, itemTs, itemText, itemStatus, itemSharedMsgId, itemDeleted, itemEdited, itemTimed, itemLive, editable, localItemTs, createdAt, updatedAt}
|
||||
in CIMeta {itemId, itemTs, itemText, itemStatus, itemSharedMsgId, itemDeleted, itemEdited, itemTimed, itemLive, editable, createdAt, updatedAt}
|
||||
|
||||
instance ToJSON (CIMeta c d) where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20230529_indexes where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20230529_indexes :: Query
|
||||
m20230529_indexes =
|
||||
[sql|
|
||||
DROP INDEX idx_chat_items_timed_delete_at;
|
||||
|
||||
CREATE INDEX idx_chat_items_timed_delete_at ON chat_items(user_id, timed_delete_at);
|
||||
|
||||
CREATE INDEX idx_group_members_group_id ON group_members(user_id, group_id);
|
||||
|
||||
CREATE INDEX idx_msg_deliveries_agent_ack_cmd_id ON msg_deliveries(connection_id, agent_ack_cmd_id);
|
||||
|]
|
||||
|
||||
down_m20230529_indexes :: Query
|
||||
down_m20230529_indexes =
|
||||
[sql|
|
||||
DROP INDEX idx_msg_deliveries_agent_ack_cmd_id;
|
||||
|
||||
DROP INDEX idx_group_members_group_id;
|
||||
|
||||
DROP INDEX idx_chat_items_timed_delete_at;
|
||||
|
||||
CREATE INDEX idx_chat_items_timed_delete_at ON chat_items(timed_delete_at);
|
||||
|]
|
||||
@@ -0,0 +1,22 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20230608_deleted_contacts where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20230608_deleted_contacts :: Query
|
||||
m20230608_deleted_contacts =
|
||||
[sql|
|
||||
ALTER TABLE contacts ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE INDEX msg_delivery_events_msg_delivery_id ON msg_delivery_events(msg_delivery_id);
|
||||
|]
|
||||
|
||||
down_m20230608_deleted_contacts :: Query
|
||||
down_m20230608_deleted_contacts =
|
||||
[sql|
|
||||
DROP INDEX msg_delivery_events_msg_delivery_id;
|
||||
|
||||
ALTER TABLE contacts DROP COLUMN deleted;
|
||||
|]
|
||||
@@ -63,6 +63,7 @@ CREATE TABLE contacts(
|
||||
contact_used INTEGER DEFAULT 0 CHECK(contact_used NOT NULL),
|
||||
user_preferences TEXT DEFAULT '{}' CHECK(user_preferences NOT NULL),
|
||||
chat_ts TEXT,
|
||||
deleted INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(user_id, local_display_name)
|
||||
REFERENCES display_names(user_id, local_display_name)
|
||||
ON DELETE CASCADE
|
||||
@@ -522,7 +523,6 @@ CREATE UNIQUE INDEX idx_snd_files_last_inline_msg_delivery_id ON snd_files(
|
||||
CREATE INDEX idx_messages_connection_id ON messages(connection_id);
|
||||
CREATE INDEX idx_chat_items_group_member_id ON chat_items(group_member_id);
|
||||
CREATE INDEX idx_chat_items_contact_id ON chat_items(contact_id);
|
||||
CREATE INDEX idx_chat_items_timed_delete_at ON chat_items(timed_delete_at);
|
||||
CREATE INDEX idx_chat_items_item_status ON chat_items(item_status);
|
||||
CREATE INDEX idx_connections_group_member ON connections(
|
||||
user_id,
|
||||
@@ -644,3 +644,15 @@ CREATE INDEX idx_messages_created_at ON messages(created_at);
|
||||
CREATE INDEX idx_chat_item_reactions_created_by_msg_id ON chat_item_reactions(
|
||||
created_by_msg_id
|
||||
);
|
||||
CREATE INDEX idx_chat_items_timed_delete_at ON chat_items(
|
||||
user_id,
|
||||
timed_delete_at
|
||||
);
|
||||
CREATE INDEX idx_group_members_group_id ON group_members(user_id, group_id);
|
||||
CREATE INDEX idx_msg_deliveries_agent_ack_cmd_id ON msg_deliveries(
|
||||
connection_id,
|
||||
agent_ack_cmd_id
|
||||
);
|
||||
CREATE INDEX msg_delivery_events_msg_delivery_id ON msg_delivery_events(
|
||||
msg_delivery_id
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ import Simplex.Chat.Controller (ChatLogLevel (..), updateStr, versionNumber, ver
|
||||
import Simplex.Messaging.Client (NetworkConfig (..), defaultNetworkConfig)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Protocol (ProtocolTypeI, ProtoServerWithAuth, SMPServerWithAuth, XFTPServerWithAuth)
|
||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth)
|
||||
import Simplex.Messaging.Transport.Client (SocksProxy, defaultSocksProxy)
|
||||
import System.FilePath (combine)
|
||||
|
||||
@@ -88,7 +88,11 @@ coreChatOptsP appDir defaultDbFileName = do
|
||||
( long "server"
|
||||
<> short 's'
|
||||
<> metavar "SERVER"
|
||||
<> help "Semicolon-separated list of SMP server(s) to use (each server can have more than one hostname)"
|
||||
<> help
|
||||
( ("Space-separated list of SMP server(s) to use (each server can have more than one hostname)." <> "\n")
|
||||
<> ("If you pass multiple servers, surround the entire list in quotes." <> "\n")
|
||||
<> "Examples: smp1.example.com, \"smp1.example.com smp2.example.com smp3.example.com\""
|
||||
)
|
||||
<> value []
|
||||
)
|
||||
xftpServers <-
|
||||
@@ -96,7 +100,11 @@ coreChatOptsP appDir defaultDbFileName = do
|
||||
parseProtocolServers
|
||||
( long "xftp-server"
|
||||
<> metavar "SERVER"
|
||||
<> help "Semicolon-separated list of XFTP server(s) to use (each server can have more than one hostname)"
|
||||
<> help
|
||||
( ("Space-separated list of XFTP server(s) to use (each server can have more than one hostname)." <> "\n")
|
||||
<> ("If you pass multiple servers, surround the entire list in quotes." <> "\n")
|
||||
<> "Examples: xftp1.example.com, \"xftp1.example.com xftp2.example.com xftp3.example.com\""
|
||||
)
|
||||
<> value []
|
||||
)
|
||||
socksProxy <-
|
||||
@@ -270,7 +278,7 @@ serverPortP :: A.Parser (Maybe String)
|
||||
serverPortP = Just . B.unpack <$> A.takeWhile A.isDigit
|
||||
|
||||
protocolServersP :: ProtocolTypeI p => A.Parser [ProtoServerWithAuth p]
|
||||
protocolServersP = strP `A.sepBy1` A.char ';'
|
||||
protocolServersP = strP `A.sepBy1` A.char ' '
|
||||
|
||||
parseLogLevel :: ReadM ChatLogLevel
|
||||
parseLogLevel = eitherReader $ \case
|
||||
|
||||
+71
-54
@@ -52,6 +52,8 @@ module Simplex.Chat.Store
|
||||
deleteContactConnectionsAndFiles,
|
||||
deleteContact,
|
||||
deleteContactWithoutGroups,
|
||||
setContactDeleted,
|
||||
getDeletedContacts,
|
||||
getContactByName,
|
||||
getContact,
|
||||
getContactIdByName,
|
||||
@@ -320,7 +322,6 @@ import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||
import Data.Time (addUTCTime)
|
||||
import Data.Time.Clock (UTCTime (..), getCurrentTime, nominalDay)
|
||||
import Data.Time.LocalTime (TimeZone, getCurrentTimeZone)
|
||||
import Data.Type.Equality
|
||||
import Database.SQLite.Simple (NamedParam (..), Only (..), Query (..), SQLError, (:.) (..))
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
@@ -395,6 +396,8 @@ import Simplex.Chat.Migrations.M20230505_chat_item_versions
|
||||
import Simplex.Chat.Migrations.M20230511_reactions
|
||||
import Simplex.Chat.Migrations.M20230519_item_deleted_ts
|
||||
import Simplex.Chat.Migrations.M20230526_indexes
|
||||
import Simplex.Chat.Migrations.M20230529_indexes
|
||||
import Simplex.Chat.Migrations.M20230608_deleted_contacts
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (week)
|
||||
@@ -475,7 +478,9 @@ schemaMigrations =
|
||||
("20230505_chat_item_versions", m20230505_chat_item_versions, Just down_m20230505_chat_item_versions),
|
||||
("20230511_reactions", m20230511_reactions, Just down_m20230511_reactions),
|
||||
("20230519_item_deleted_ts", m20230519_item_deleted_ts, Just down_m20230519_item_deleted_ts),
|
||||
("20230526_indexes", m20230526_indexes, Just down_m20230526_indexes)
|
||||
("20230526_indexes", m20230526_indexes, Just down_m20230526_indexes),
|
||||
("20230529_indexes", m20230529_indexes, Just down_m20230529_indexes),
|
||||
("20230608_deleted_contacts", m20230608_deleted_contacts, Just down_m20230608_deleted_contacts)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
@@ -546,7 +551,7 @@ getUsersInfo db = getUsers db >>= mapM getUserInfo
|
||||
SELECT COUNT(1)
|
||||
FROM chat_items i
|
||||
JOIN contacts ct USING (contact_id)
|
||||
WHERE i.user_id = ? AND i.item_status = ? AND (ct.enable_ntfs = 1 OR ct.enable_ntfs IS NULL)
|
||||
WHERE i.user_id = ? AND i.item_status = ? AND (ct.enable_ntfs = 1 OR ct.enable_ntfs IS NULL) AND ct.deleted = 0
|
||||
|]
|
||||
(userId, CISRcvNew)
|
||||
gCount <-
|
||||
@@ -621,7 +626,7 @@ getUserByARcvFileId db aRcvFileId =
|
||||
getUserByContactId :: DB.Connection -> ContactId -> ExceptT StoreError IO User
|
||||
getUserByContactId db contactId =
|
||||
ExceptT . firstRow toUser (SEUserNotFoundByContactId contactId) $
|
||||
DB.query db (userQuery <> " JOIN contacts ct ON ct.user_id = u.user_id WHERE ct.contact_id = ?") (Only contactId)
|
||||
DB.query db (userQuery <> " JOIN contacts ct ON ct.user_id = u.user_id WHERE ct.contact_id = ? AND ct.deleted = 0") (Only contactId)
|
||||
|
||||
getUserByGroupId :: DB.Connection -> GroupId -> ExceptT StoreError IO User
|
||||
getUserByGroupId db groupId =
|
||||
@@ -710,7 +715,7 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do
|
||||
FROM contacts ct
|
||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||
JOIN connections c ON c.contact_id = ct.contact_id
|
||||
WHERE ct.user_id = ? AND c.via_contact_uri_hash = ?
|
||||
WHERE ct.user_id = ? AND c.via_contact_uri_hash = ? AND ct.deleted = 0
|
||||
ORDER BY c.connection_id DESC
|
||||
LIMIT 1
|
||||
|]
|
||||
@@ -756,7 +761,6 @@ getProfileById db userId profileId =
|
||||
[sql|
|
||||
SELECT cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, cp.preferences -- , ct.user_preferences
|
||||
FROM contact_profiles cp
|
||||
-- JOIN contacts ct ON cp.contact_profile_id = ct.contact_profile_id
|
||||
WHERE cp.user_id = ? AND cp.contact_profile_id = ?
|
||||
|]
|
||||
(userId, profileId)
|
||||
@@ -848,6 +852,19 @@ deleteContactWithoutGroups db user@User {userId} Contact {contactId, localDispla
|
||||
DB.execute db "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?" (userId, contactId)
|
||||
forM_ customUserProfileId $ \profileId -> deleteUnusedIncognitoProfileById_ db user profileId
|
||||
|
||||
setContactDeleted :: DB.Connection -> User -> Contact -> IO ()
|
||||
setContactDeleted db User {userId} Contact {contactId} = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute db "UPDATE contacts SET deleted = 1, updated_at = ? WHERE user_id = ? AND contact_id = ?" (currentTs, userId, contactId)
|
||||
|
||||
getDeletedContacts :: DB.Connection -> User -> IO [Contact]
|
||||
getDeletedContacts db user@User {userId} = do
|
||||
contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 1" (Only userId)
|
||||
rights <$> mapM (runExceptT . getDeletedContact db user) contactIds
|
||||
|
||||
getDeletedContact :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO Contact
|
||||
getDeletedContact db user contactId = getContact_ db user contactId True
|
||||
|
||||
deleteUnusedIncognitoProfileById_ :: DB.Connection -> User -> ProfileId -> IO ()
|
||||
deleteUnusedIncognitoProfileById_ db User {userId} profile_id =
|
||||
DB.executeNamed
|
||||
@@ -1060,7 +1077,7 @@ getContactByName db user localDisplayName = do
|
||||
|
||||
getUserContacts :: DB.Connection -> User -> IO [Contact]
|
||||
getUserContacts db user@User {userId} = do
|
||||
contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ?" (Only userId)
|
||||
contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0" (Only userId)
|
||||
rights <$> mapM (runExceptT . getContact db user) contactIds
|
||||
|
||||
-- only used in tests
|
||||
@@ -1364,7 +1381,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
|
||||
FROM contacts ct
|
||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
||||
WHERE ct.user_id = ? AND ct.xcontact_id = ?
|
||||
WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0
|
||||
ORDER BY c.connection_id DESC
|
||||
LIMIT 1
|
||||
|]
|
||||
@@ -1614,6 +1631,7 @@ getMatchingContacts db user@User {userId} Contact {contactId, profile = LocalPro
|
||||
FROM contacts ct
|
||||
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
|
||||
WHERE ct.user_id = ? AND ct.contact_id != ?
|
||||
AND ct.deleted = 0
|
||||
AND p.display_name = ? AND p.full_name = ?
|
||||
AND ((p.image IS NULL AND ? IS NULL) OR p.image = ?)
|
||||
|]
|
||||
@@ -1656,7 +1674,7 @@ matchReceivedProbe db user@User {userId} _from@Contact {contactId} (Probe probe)
|
||||
SELECT c.contact_id
|
||||
FROM contacts c
|
||||
JOIN received_probes r ON r.contact_id = c.contact_id
|
||||
WHERE c.user_id = ? AND r.probe_hash = ? AND r.probe IS NULL
|
||||
WHERE c.user_id = ? AND c.deleted = 0 AND r.probe_hash = ? AND r.probe IS NULL
|
||||
|]
|
||||
(userId, probeHash)
|
||||
currentTs <- getCurrentTime
|
||||
@@ -1677,7 +1695,7 @@ matchReceivedProbeHash db user@User {userId} _from@Contact {contactId} (ProbeHas
|
||||
SELECT c.contact_id, r.probe
|
||||
FROM contacts c
|
||||
JOIN received_probes r ON r.contact_id = c.contact_id
|
||||
WHERE c.user_id = ? AND r.probe_hash = ? AND r.probe IS NOT NULL
|
||||
WHERE c.user_id = ? AND c.deleted = 0 AND r.probe_hash = ? AND r.probe IS NOT NULL
|
||||
|]
|
||||
(userId, probeHash)
|
||||
currentTs <- getCurrentTime
|
||||
@@ -1702,7 +1720,7 @@ matchSentProbe db user@User {userId} _from@Contact {contactId} (Probe probe) = d
|
||||
FROM contacts c
|
||||
JOIN sent_probes s ON s.contact_id = c.contact_id
|
||||
JOIN sent_probe_hashes h ON h.sent_probe_id = s.sent_probe_id
|
||||
WHERE c.user_id = ? AND s.probe = ? AND h.contact_id = ?
|
||||
WHERE c.user_id = ? AND c.deleted = 0 AND s.probe = ? AND h.contact_id = ?
|
||||
|]
|
||||
(userId, probe, contactId)
|
||||
case contactIds of
|
||||
@@ -1807,7 +1825,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
|
||||
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts
|
||||
FROM contacts c
|
||||
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
|
||||
WHERE c.user_id = ? AND c.contact_id = ?
|
||||
WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0
|
||||
|]
|
||||
(userId, contactId)
|
||||
toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, Maybe Bool) :. (Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime)] -> Either StoreError Contact
|
||||
@@ -1912,6 +1930,7 @@ getConnectionsContacts db agentConnIds = do
|
||||
JOIN connections c ON c.contact_id = ct.contact_id
|
||||
WHERE c.agent_conn_id IN (SELECT conn_id FROM temp.conn_ids)
|
||||
AND c.conn_type = ?
|
||||
AND ct.deleted = 0
|
||||
|]
|
||||
(Only ConnContact)
|
||||
DB.execute_ db "DROP TABLE temp.conn_ids"
|
||||
@@ -2387,7 +2406,7 @@ getContactViaMember db user@User {userId} GroupMember {groupMemberId} =
|
||||
where cc.contact_id = ct.contact_id
|
||||
)
|
||||
JOIN group_members m ON m.contact_id = ct.contact_id
|
||||
WHERE ct.user_id = ? AND m.group_member_id = ?
|
||||
WHERE ct.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0
|
||||
|]
|
||||
(userId, groupMemberId)
|
||||
|
||||
@@ -2696,7 +2715,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} =
|
||||
FROM connections cc
|
||||
where cc.group_member_id = m.group_member_id
|
||||
)
|
||||
WHERE ct.user_id = ? AND ct.contact_id = ? AND mu.contact_id = ?
|
||||
WHERE ct.user_id = ? AND ct.contact_id = ? AND mu.contact_id = ? AND ct.deleted = 0
|
||||
|]
|
||||
(userId, contactId, userContactId)
|
||||
where
|
||||
@@ -2726,7 +2745,7 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} =
|
||||
)
|
||||
JOIN groups g ON g.group_id = ct.via_group
|
||||
JOIN group_members m ON m.group_id = g.group_id AND m.contact_id = ct.contact_id
|
||||
WHERE ct.user_id = ? AND m.group_member_id = ?
|
||||
WHERE ct.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0
|
||||
|]
|
||||
(userId, groupMemberId)
|
||||
where
|
||||
@@ -3785,9 +3804,8 @@ getChatPreviews db user withPCC = do
|
||||
|
||||
getDirectChatPreviews_ :: DB.Connection -> User -> IO [AChat]
|
||||
getDirectChatPreviews_ db user@User {userId} = do
|
||||
tz <- getCurrentTimeZone
|
||||
currentTs <- getCurrentTime
|
||||
map (toDirectChatPreview tz currentTs)
|
||||
map (toDirectChatPreview currentTs)
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
@@ -3826,6 +3844,7 @@ getDirectChatPreviews_ db user@User {userId} = do
|
||||
LEFT JOIN chat_items ri ON ri.user_id = i.user_id AND ri.contact_id = i.contact_id AND ri.shared_msg_id = i.quoted_shared_msg_id
|
||||
WHERE ct.user_id = ?
|
||||
AND ((c.conn_level = 0 AND c.via_group_link = 0) OR ct.contact_used = 1)
|
||||
AND ct.deleted = 0
|
||||
AND c.connection_id = (
|
||||
SELECT cc_connection_id FROM (
|
||||
SELECT
|
||||
@@ -3841,18 +3860,17 @@ getDirectChatPreviews_ db user@User {userId} = do
|
||||
|]
|
||||
(CISRcvNew, userId, ConnReady, ConnSndReady)
|
||||
where
|
||||
toDirectChatPreview :: TimeZone -> UTCTime -> ContactRow :. ConnectionRow :. ChatStatsRow :. MaybeChatItemRow :. QuoteRow -> AChat
|
||||
toDirectChatPreview tz currentTs (contactRow :. connRow :. statsRow :. ciRow_) =
|
||||
toDirectChatPreview :: UTCTime -> ContactRow :. ConnectionRow :. ChatStatsRow :. MaybeChatItemRow :. QuoteRow -> AChat
|
||||
toDirectChatPreview currentTs (contactRow :. connRow :. statsRow :. ciRow_) =
|
||||
let contact = toContact user $ contactRow :. connRow
|
||||
ci_ = toDirectChatItemList tz currentTs ciRow_
|
||||
ci_ = toDirectChatItemList currentTs ciRow_
|
||||
stats = toChatStats statsRow
|
||||
in AChat SCTDirect $ Chat (DirectChat contact) ci_ stats
|
||||
|
||||
getGroupChatPreviews_ :: DB.Connection -> User -> IO [AChat]
|
||||
getGroupChatPreviews_ db User {userId, userContactId} = do
|
||||
tz <- getCurrentTimeZone
|
||||
currentTs <- getCurrentTime
|
||||
map (toGroupChatPreview tz currentTs)
|
||||
map (toGroupChatPreview currentTs)
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
@@ -3913,10 +3931,10 @@ getGroupChatPreviews_ db User {userId, userContactId} = do
|
||||
|]
|
||||
(CISRcvNew, userId, userContactId)
|
||||
where
|
||||
toGroupChatPreview :: TimeZone -> UTCTime -> GroupInfoRow :. ChatStatsRow :. MaybeGroupChatItemRow -> AChat
|
||||
toGroupChatPreview tz currentTs (groupInfoRow :. statsRow :. ciRow_) =
|
||||
toGroupChatPreview :: UTCTime -> GroupInfoRow :. ChatStatsRow :. MaybeGroupChatItemRow -> AChat
|
||||
toGroupChatPreview currentTs (groupInfoRow :. statsRow :. ciRow_) =
|
||||
let groupInfo = toGroupInfo userContactId groupInfoRow
|
||||
ci_ = toGroupChatItemList tz currentTs userContactId ciRow_
|
||||
ci_ = toGroupChatItemList currentTs userContactId ciRow_
|
||||
stats = toChatStats statsRow
|
||||
in AChat SCTGroup $ Chat (GroupChat groupInfo) ci_ stats
|
||||
|
||||
@@ -4024,9 +4042,8 @@ getDirectChatLast_ db user ct@Contact {contactId} count search = do
|
||||
-- the last items in reverse order (the last item in the conversation is the first in the returned list)
|
||||
getDirectChatItemsLast :: DB.Connection -> User -> ContactId -> Int -> String -> ExceptT StoreError IO [CChatItem 'CTDirect]
|
||||
getDirectChatItemsLast db User {userId} contactId count search = ExceptT $ do
|
||||
tz <- getCurrentTimeZone
|
||||
currentTs <- getCurrentTime
|
||||
mapM (toDirectChatItem tz currentTs)
|
||||
mapM (toDirectChatItem currentTs)
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
@@ -4054,9 +4071,8 @@ getDirectChatAfter_ db User {userId} ct@Contact {contactId} afterChatItemId coun
|
||||
where
|
||||
getDirectChatItemsAfter_ :: IO (Either StoreError [CChatItem 'CTDirect])
|
||||
getDirectChatItemsAfter_ = do
|
||||
tz <- getCurrentTimeZone
|
||||
currentTs <- getCurrentTime
|
||||
mapM (toDirectChatItem tz currentTs)
|
||||
mapM (toDirectChatItem currentTs)
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
@@ -4085,9 +4101,8 @@ getDirectChatBefore_ db User {userId} ct@Contact {contactId} beforeChatItemId co
|
||||
where
|
||||
getDirectChatItemsBefore_ :: IO (Either StoreError [CChatItem 'CTDirect])
|
||||
getDirectChatItemsBefore_ = do
|
||||
tz <- getCurrentTimeZone
|
||||
currentTs <- getCurrentTime
|
||||
mapM (toDirectChatItem tz currentTs)
|
||||
mapM (toDirectChatItem currentTs)
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
@@ -4111,10 +4126,13 @@ getDirectChatBefore_ db User {userId} ct@Contact {contactId} beforeChatItemId co
|
||||
getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64
|
||||
getContactIdByName db User {userId} cName =
|
||||
ExceptT . firstRow fromOnly (SEContactNotFoundByName cName) $
|
||||
DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND local_display_name = ?" (userId, cName)
|
||||
DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND local_display_name = ? AND deleted = 0" (userId, cName)
|
||||
|
||||
getContact :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO Contact
|
||||
getContact db user@User {userId} contactId =
|
||||
getContact db user contactId = getContact_ db user contactId False
|
||||
|
||||
getContact_ :: DB.Connection -> User -> Int64 -> Bool -> ExceptT StoreError IO Contact
|
||||
getContact_ db user@User {userId} contactId deleted =
|
||||
ExceptT . fmap join . firstRow (toContactOrError user) (SEContactNotFound contactId) $
|
||||
DB.query
|
||||
db
|
||||
@@ -4130,6 +4148,7 @@ getContact db user@User {userId} contactId =
|
||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
||||
WHERE ct.user_id = ? AND ct.contact_id = ?
|
||||
AND ct.deleted = ?
|
||||
AND c.connection_id = (
|
||||
SELECT cc_connection_id FROM (
|
||||
SELECT
|
||||
@@ -4142,7 +4161,7 @@ getContact db user@User {userId} contactId =
|
||||
)
|
||||
)
|
||||
|]
|
||||
(userId, contactId, ConnReady, ConnSndReady)
|
||||
(userId, contactId, deleted, ConnReady, ConnSndReady)
|
||||
|
||||
getGroupChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup)
|
||||
getGroupChat db user groupId pagination search_ = do
|
||||
@@ -4521,9 +4540,8 @@ getDirectChatItemIdBySharedMsgId_ db userId contactId sharedMsgId =
|
||||
|
||||
getDirectChatItem :: DB.Connection -> User -> Int64 -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTDirect)
|
||||
getDirectChatItem db User {userId} contactId itemId = ExceptT $ do
|
||||
tz <- getCurrentTimeZone
|
||||
currentTs <- getCurrentTime
|
||||
join <$> firstRow (toDirectChatItem tz currentTs) (SEChatItemNotFound itemId) getItem
|
||||
join <$> firstRow (toDirectChatItem currentTs) (SEChatItemNotFound itemId) getItem
|
||||
where
|
||||
getItem =
|
||||
DB.query
|
||||
@@ -4682,9 +4700,8 @@ getGroupMemberCIBySharedMsgId db user@User {userId} groupId memberId sharedMsgId
|
||||
|
||||
getGroupChatItem :: DB.Connection -> User -> Int64 -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTGroup)
|
||||
getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
|
||||
tz <- getCurrentTimeZone
|
||||
currentTs <- getCurrentTime
|
||||
join <$> firstRow (toGroupChatItem tz currentTs userContactId) (SEChatItemNotFound itemId) getItem
|
||||
join <$> firstRow (toGroupChatItem currentTs userContactId) (SEChatItemNotFound itemId) getItem
|
||||
where
|
||||
getItem =
|
||||
DB.query
|
||||
@@ -5133,8 +5150,8 @@ toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir
|
||||
CIQuote <$> dir <*> pure quotedItemId <*> pure quotedSharedMsgId <*> quotedSentAt <*> quotedMsgContent <*> (parseMaybeMarkdownList . msgContentText <$> quotedMsgContent)
|
||||
|
||||
-- this function can be changed so it never fails, not only avoid failure on invalid json
|
||||
toDirectChatItem :: TimeZone -> UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect)
|
||||
toDirectChatItem tz currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileStatus_, fileProtocol_)) :. quoteRow) =
|
||||
toDirectChatItem :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect)
|
||||
toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileStatus_, fileProtocol_)) :. quoteRow) =
|
||||
chatItem $ fromRight invalid $ dbParseACIContent itemContentText
|
||||
where
|
||||
invalid = ACIContent msgDir $ CIInvalidJSON itemContentText
|
||||
@@ -5161,14 +5178,14 @@ toDirectChatItem tz currentTs (((itemId, itemTs, AMsgDirection msgDir, itemConte
|
||||
ciMeta content status =
|
||||
let itemDeleted' = if itemDeleted then Just (CIDeleted @'CTDirect deletedTs) else Nothing
|
||||
itemEdited' = fromMaybe False itemEdited
|
||||
in mkCIMeta itemId content itemText status sharedMsgId itemDeleted' itemEdited' ciTimed itemLive tz currentTs itemTs createdAt updatedAt
|
||||
in mkCIMeta itemId content itemText status sharedMsgId itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs createdAt updatedAt
|
||||
ciTimed :: Maybe CITimed
|
||||
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
|
||||
|
||||
toDirectChatItemList :: TimeZone -> UTCTime -> MaybeChatItemRow :. QuoteRow -> [CChatItem 'CTDirect]
|
||||
toDirectChatItemList tz currentTs (((Just itemId, Just itemTs, Just msgDir, Just itemContent, Just itemText, Just itemStatus, sharedMsgId) :. (Just itemDeleted, deletedTs, itemEdited, Just createdAt, Just updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. quoteRow) =
|
||||
either (const []) (: []) $ toDirectChatItem tz currentTs (((itemId, itemTs, msgDir, itemContent, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. quoteRow)
|
||||
toDirectChatItemList _ _ _ = []
|
||||
toDirectChatItemList :: UTCTime -> MaybeChatItemRow :. QuoteRow -> [CChatItem 'CTDirect]
|
||||
toDirectChatItemList currentTs (((Just itemId, Just itemTs, Just msgDir, Just itemContent, Just itemText, Just itemStatus, sharedMsgId) :. (Just itemDeleted, deletedTs, itemEdited, Just createdAt, Just updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. quoteRow) =
|
||||
either (const []) (: []) $ toDirectChatItem currentTs (((itemId, itemTs, msgDir, itemContent, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. quoteRow)
|
||||
toDirectChatItemList _ _ = []
|
||||
|
||||
type GroupQuoteRow = QuoteRow :. MaybeGroupMemberRow
|
||||
|
||||
@@ -5183,8 +5200,8 @@ toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction
|
||||
direction _ _ = Nothing
|
||||
|
||||
-- this function can be changed so it never fails, not only avoid failure on invalid json
|
||||
toGroupChatItem :: TimeZone -> UTCTime -> Int64 -> ChatItemRow :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup)
|
||||
toGroupChatItem tz currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileStatus_, fileProtocol_)) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do
|
||||
toGroupChatItem :: UTCTime -> Int64 -> ChatItemRow :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup)
|
||||
toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileStatus_, fileProtocol_)) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do
|
||||
chatItem $ fromRight invalid $ dbParseACIContent itemContentText
|
||||
where
|
||||
member_ = toMaybeGroupMember userContactId memberRow_
|
||||
@@ -5217,14 +5234,14 @@ toGroupChatItem tz currentTs userContactId (((itemId, itemTs, AMsgDirection msgD
|
||||
then Just (maybe (CIDeleted @'CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_)
|
||||
else Nothing
|
||||
itemEdited' = fromMaybe False itemEdited
|
||||
in mkCIMeta itemId content itemText status sharedMsgId itemDeleted' itemEdited' ciTimed itemLive tz currentTs itemTs createdAt updatedAt
|
||||
in mkCIMeta itemId content itemText status sharedMsgId itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs createdAt updatedAt
|
||||
ciTimed :: Maybe CITimed
|
||||
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
|
||||
|
||||
toGroupChatItemList :: TimeZone -> UTCTime -> Int64 -> MaybeGroupChatItemRow -> [CChatItem 'CTGroup]
|
||||
toGroupChatItemList tz currentTs userContactId (((Just itemId, Just itemTs, Just msgDir, Just itemContent, Just itemText, Just itemStatus, sharedMsgId) :. (Just itemDeleted, deletedTs, itemEdited, Just createdAt, Just updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) =
|
||||
either (const []) (: []) $ toGroupChatItem tz currentTs userContactId (((itemId, itemTs, msgDir, itemContent, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_)
|
||||
toGroupChatItemList _ _ _ _ = []
|
||||
toGroupChatItemList :: UTCTime -> Int64 -> MaybeGroupChatItemRow -> [CChatItem 'CTGroup]
|
||||
toGroupChatItemList currentTs userContactId (((Just itemId, Just itemTs, Just msgDir, Just itemContent, Just itemText, Just itemStatus, sharedMsgId) :. (Just itemDeleted, deletedTs, itemEdited, Just createdAt, Just updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) =
|
||||
either (const []) (: []) $ toGroupChatItem currentTs userContactId (((itemId, itemTs, msgDir, itemContent, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_)
|
||||
toGroupChatItemList _ _ _ = []
|
||||
|
||||
getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> IO [ServerCfg p]
|
||||
getProtocolServers db User {userId} =
|
||||
@@ -5383,7 +5400,7 @@ getXGrpMemIntroContDirect db User {userId} Contact {contactId} = do
|
||||
FROM connections cc
|
||||
where cc.group_member_id = mh.group_member_id
|
||||
)
|
||||
WHERE ct.user_id = ? AND ct.contact_id = ? AND mh.member_category = ?
|
||||
WHERE ct.user_id = ? AND ct.contact_id = ? AND ct.deleted = 0 AND mh.member_category = ?
|
||||
|]
|
||||
(userId, contactId, GCHostMember)
|
||||
where
|
||||
@@ -5413,7 +5430,7 @@ getXGrpMemIntroContGroup db User {userId} GroupMember {groupMemberId} = do
|
||||
FROM connections cc
|
||||
where cc.group_member_id = mh.group_member_id
|
||||
)
|
||||
WHERE m.user_id = ? AND m.group_member_id = ? AND mh.member_category = ?
|
||||
WHERE m.user_id = ? AND m.group_member_id = ? AND mh.member_category = ? AND ct.deleted = 0
|
||||
|]
|
||||
(userId, groupMemberId, GCHostMember)
|
||||
where
|
||||
|
||||
+78
-71
@@ -24,9 +24,10 @@ import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeLatin1)
|
||||
import Data.Time.Clock (DiffTime, UTCTime)
|
||||
import Data.Time (LocalTime (..), TimeOfDay (..), TimeZone (..), utcToLocalTime)
|
||||
import Data.Time.Calendar (addDays)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Data.Time.Format (defaultTimeLocale, formatTime)
|
||||
import Data.Time.LocalTime (TimeZone, ZonedTime (..), localDay, localTimeOfDay, timeOfDayToTime, utcToLocalTime, utcToZonedTime)
|
||||
import Data.Word (Word32)
|
||||
import GHC.Generics (Generic)
|
||||
import qualified Network.HTTP.Types as Q
|
||||
@@ -70,7 +71,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, testView} liveItems ts
|
||||
CRChatStopped -> ["chat stopped"]
|
||||
CRChatSuspended -> ["chat suspended"]
|
||||
CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [plain . bshow $ J.encode chats]
|
||||
CRChats chats -> viewChats ts chats
|
||||
CRChats chats -> viewChats ts tz chats
|
||||
CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [plain . bshow $ J.encode chat]
|
||||
CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft]
|
||||
CRUserProtoServers u userServers -> ttyUser u $ viewUserServers userServers testView
|
||||
@@ -84,17 +85,17 @@ responseToView user_ ChatConfig {logLevel, showReactions, testView} liveItems ts
|
||||
CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code]
|
||||
CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView
|
||||
CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView
|
||||
CRNewChatItem u (AChatItem _ _ chat item) -> ttyUser u $ unmuted chat item $ viewChatItem chat item False ts <> viewItemReactions item
|
||||
CRChatItems u chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts <> viewItemReactions item) chatItems
|
||||
CRNewChatItem u (AChatItem _ _ chat item) -> ttyUser u $ unmuted chat item $ viewChatItem chat item False ts tz <> viewItemReactions item
|
||||
CRChatItems u chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts tz <> viewItemReactions item) chatItems
|
||||
CRChatItemInfo u ci ciInfo -> ttyUser u $ viewChatItemInfo ci ciInfo tz
|
||||
CRChatItemId u itemId -> ttyUser u [plain $ maybe "no item" show itemId]
|
||||
CRChatItemStatusUpdated u _ -> ttyUser u []
|
||||
CRChatItemUpdated u (AChatItem _ _ chat item) -> ttyUser u $ unmuted chat item $ viewItemUpdate chat item liveItems ts
|
||||
CRChatItemUpdated u (AChatItem _ _ chat item) -> ttyUser u $ unmuted chat item $ viewItemUpdate chat item liveItems ts tz
|
||||
CRChatItemNotChanged u ci -> ttyUser u $ viewItemNotChanged ci
|
||||
CRChatItemDeleted u (AChatItem _ _ chat deletedItem) toItem byUser timed -> ttyUser u $ unmuted chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts testView
|
||||
CRChatItemDeleted u (AChatItem _ _ chat deletedItem) toItem byUser timed -> ttyUser u $ unmuted chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts tz testView
|
||||
CRChatItemReaction u added (ACIReaction _ _ chat reaction) -> ttyUser u $ unmutedReaction chat reaction $ viewItemReaction showReactions chat reaction added ts tz
|
||||
CRChatItemDeletedNotFound u Contact {localDisplayName = c} _ -> ttyUser u [ttyFrom $ c <> "> [deleted - original message not found]"]
|
||||
CRBroadcastSent u mc n t -> ttyUser u $ viewSentBroadcast mc n ts t
|
||||
CRBroadcastSent u mc n t -> ttyUser u $ viewSentBroadcast mc n ts tz t
|
||||
CRMsgIntegrityError u mErr -> ttyUser u $ viewMsgIntegrityError mErr
|
||||
CRCmdAccepted _ -> []
|
||||
CRCmdOk u_ -> ttyUser' u_ ["ok"]
|
||||
@@ -194,7 +195,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, testView} liveItems ts
|
||||
CRHostConnected p h -> [plain $ "connected to " <> viewHostEvent p h]
|
||||
CRHostDisconnected p h -> [plain $ "disconnected from " <> viewHostEvent p h]
|
||||
CRJoinedGroupMemberConnecting u g host m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"]
|
||||
CRConnectedToGroupMember u g m -> ttyUser u [ttyGroup' g <> ": " <> connectedMember m <> " is connected"]
|
||||
CRConnectedToGroupMember u g m _ -> ttyUser u [ttyGroup' g <> ": " <> connectedMember m <> " is connected"]
|
||||
CRMemberRole u g by m r r' -> ttyUser u $ viewMemberRoleChanged g by m r r'
|
||||
CRMemberRoleUser u g m r r' -> ttyUser u $ viewMemberRoleUserChanged g m r r'
|
||||
CRDeletedMemberUser u g by -> ttyUser u $ [ttyGroup' g <> ": " <> ttyMember by <> " removed you from the group"] <> groupPreserved g
|
||||
@@ -352,11 +353,11 @@ showSMPServer = B.unpack . strEncode . host
|
||||
viewHostEvent :: AProtocolType -> TransportHost -> String
|
||||
viewHostEvent p h = map toUpper (B.unpack $ strEncode p) <> " host " <> B.unpack (strEncode h)
|
||||
|
||||
viewChats :: CurrentTime -> [AChat] -> [StyledString]
|
||||
viewChats ts = concatMap chatPreview . reverse
|
||||
viewChats :: CurrentTime -> TimeZone -> [AChat] -> [StyledString]
|
||||
viewChats ts tz = concatMap chatPreview . reverse
|
||||
where
|
||||
chatPreview (AChat _ (Chat chat items _)) = case items of
|
||||
CChatItem _ ci : _ -> case viewChatItem chat ci True ts of
|
||||
CChatItem _ ci : _ -> case viewChatItem chat ci True ts tz of
|
||||
s : _ -> [let s' = sTake 120 s in if sLength s' < sLength s then s' <> "..." else s']
|
||||
_ -> chatName
|
||||
_ -> chatName
|
||||
@@ -366,8 +367,8 @@ viewChats ts = concatMap chatPreview . reverse
|
||||
GroupChat g -> [" " <> ttyToGroup g]
|
||||
_ -> []
|
||||
|
||||
viewChatItem :: forall c d. MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> [StyledString]
|
||||
viewChatItem chat ci@ChatItem {chatDir, meta = meta, content, quotedItem, file} doShow ts =
|
||||
viewChatItem :: forall c d. MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> TimeZone -> [StyledString]
|
||||
viewChatItem chat ci@ChatItem {chatDir, meta = meta, content, quotedItem, file} doShow ts tz =
|
||||
withItemDeleted <$> case chat of
|
||||
DirectChat c -> case chatDir of
|
||||
CIDirectSnd -> case content of
|
||||
@@ -378,8 +379,8 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta, content, quotedItem, file}
|
||||
to = ttyToContact' c
|
||||
CIDirectRcv -> case content of
|
||||
CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc
|
||||
CIRcvIntegrityError err -> viewRcvIntegrityError from err ts meta
|
||||
CIRcvDecryptionError err n -> viewRcvDecryptionError from err n ts meta
|
||||
CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta
|
||||
CIRcvDecryptionError err n -> viewRcvDecryptionError from err n ts tz meta
|
||||
CIRcvGroupEvent {} -> showRcvItemProhibited from
|
||||
_ -> showRcvItem from
|
||||
where
|
||||
@@ -395,8 +396,8 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta, content, quotedItem, file}
|
||||
to = ttyToGroup g
|
||||
CIGroupRcv m -> case content of
|
||||
CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc
|
||||
CIRcvIntegrityError err -> viewRcvIntegrityError from err ts meta
|
||||
CIRcvDecryptionError err n -> viewRcvDecryptionError from err n ts meta
|
||||
CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta
|
||||
CIRcvDecryptionError err n -> viewRcvDecryptionError from err n ts tz meta
|
||||
CIRcvGroupInvitation {} -> showRcvItemProhibited from
|
||||
_ -> showRcvItem from
|
||||
where
|
||||
@@ -410,17 +411,17 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta, content, quotedItem, file}
|
||||
Just t -> item <> styled (colored Red) (" [" <> t <> "]")
|
||||
withSndFile = withFile viewSentFileInvitation
|
||||
withRcvFile = withFile viewReceivedFileInvitation
|
||||
withFile view dir l = maybe l (\f -> l <> view dir f ts meta) file
|
||||
withFile view dir l = maybe l (\f -> l <> view dir f ts tz meta) file
|
||||
sndMsg = msg viewSentMessage
|
||||
rcvMsg = msg viewReceivedMessage
|
||||
msg view dir quote mc = case (msgContentText mc, file, quote) of
|
||||
("", Just _, []) -> []
|
||||
("", Just CIFile {fileName}, _) -> view dir quote (MCText $ T.pack fileName) ts meta
|
||||
_ -> view dir quote mc ts meta
|
||||
showSndItem to = showItem $ sentWithTime_ ts [to <> plainContent content] meta
|
||||
showRcvItem from = showItem $ receivedWithTime_ ts from [] meta [plainContent content] False
|
||||
showSndItemProhibited to = showItem $ sentWithTime_ ts [to <> plainContent content <> " " <> prohibited] meta
|
||||
showRcvItemProhibited from = showItem $ receivedWithTime_ ts from [] meta [plainContent content <> " " <> prohibited] False
|
||||
("", Just CIFile {fileName}, _) -> view dir quote (MCText $ T.pack fileName) ts tz meta
|
||||
_ -> view dir quote mc ts tz meta
|
||||
showSndItem to = showItem $ sentWithTime_ ts tz [to <> plainContent content] meta
|
||||
showRcvItem from = showItem $ receivedWithTime_ ts tz from [] meta [plainContent content] False
|
||||
showSndItemProhibited to = showItem $ sentWithTime_ ts tz [to <> plainContent content <> " " <> prohibited] meta
|
||||
showRcvItemProhibited from = showItem $ receivedWithTime_ ts tz from [] meta [plainContent content <> " " <> prohibited] False
|
||||
showItem ss = if doShow then ss else []
|
||||
plainContent = plain . ciContentToText
|
||||
prohibited = styled (colored Red) ("[unexpected chat item created, please report to developers]" :: String)
|
||||
@@ -451,18 +452,18 @@ localTs tz ts = do
|
||||
formattedTime = formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S" localTime
|
||||
formattedTime
|
||||
|
||||
viewItemUpdate :: MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> [StyledString]
|
||||
viewItemUpdate chat ChatItem {chatDir, meta = meta@CIMeta {itemEdited, itemLive}, content, quotedItem} liveItems ts = case chat of
|
||||
viewItemUpdate :: MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> TimeZone -> [StyledString]
|
||||
viewItemUpdate chat ChatItem {chatDir, meta = meta@CIMeta {itemEdited, itemLive}, content, quotedItem} liveItems ts tz = case chat of
|
||||
DirectChat c -> case chatDir of
|
||||
CIDirectRcv -> case content of
|
||||
CIRcvMsgContent mc
|
||||
| itemLive == Just True && not liveItems -> []
|
||||
| otherwise -> viewReceivedUpdatedMessage from quote mc ts meta
|
||||
| otherwise -> viewReceivedUpdatedMessage from quote mc ts tz meta
|
||||
_ -> []
|
||||
where
|
||||
from = if itemEdited then ttyFromContactEdited c else ttyFromContact c
|
||||
CIDirectSnd -> case content of
|
||||
CISndMsgContent mc -> hideLive meta $ viewSentMessage to quote mc ts meta
|
||||
CISndMsgContent mc -> hideLive meta $ viewSentMessage to quote mc ts tz meta
|
||||
_ -> []
|
||||
where
|
||||
to = if itemEdited then ttyToContactEdited' c else ttyToContact' c
|
||||
@@ -472,12 +473,12 @@ viewItemUpdate chat ChatItem {chatDir, meta = meta@CIMeta {itemEdited, itemLive}
|
||||
CIGroupRcv m -> case content of
|
||||
CIRcvMsgContent mc
|
||||
| itemLive == Just True && not liveItems -> []
|
||||
| otherwise -> viewReceivedUpdatedMessage from quote mc ts meta
|
||||
| otherwise -> viewReceivedUpdatedMessage from quote mc ts tz meta
|
||||
_ -> []
|
||||
where
|
||||
from = if itemEdited then ttyFromGroupEdited g m else ttyFromGroup g m
|
||||
CIGroupSnd -> case content of
|
||||
CISndMsgContent mc -> hideLive meta $ viewSentMessage to quote mc ts meta
|
||||
CISndMsgContent mc -> hideLive meta $ viewSentMessage to quote mc ts tz meta
|
||||
_ -> []
|
||||
where
|
||||
to = if itemEdited then ttyToGroupEdited g else ttyToGroup g
|
||||
@@ -494,18 +495,18 @@ viewItemNotChanged (AChatItem _ msgDir _ _) = case msgDir of
|
||||
SMDSnd -> ["message didn't change"]
|
||||
SMDRcv -> []
|
||||
|
||||
viewItemDelete :: ChatInfo c -> ChatItem c d -> Maybe AChatItem -> Bool -> Bool -> CurrentTime -> Bool -> [StyledString]
|
||||
viewItemDelete chat ci@ChatItem {chatDir, meta, content = deletedContent} toItem byUser timed ts testView
|
||||
viewItemDelete :: ChatInfo c -> ChatItem c d -> Maybe AChatItem -> Bool -> Bool -> CurrentTime -> TimeZone -> Bool -> [StyledString]
|
||||
viewItemDelete chat ci@ChatItem {chatDir, meta, content = deletedContent} toItem byUser timed ts tz testView
|
||||
| timed = [plain ("timed message deleted: " <> T.unpack (ciContentToText deletedContent)) | testView]
|
||||
| byUser = [plain $ "message " <> T.unpack (fromMaybe "deleted" deletedText_)] -- deletedText_ Nothing should be impossible here
|
||||
| otherwise = case chat of
|
||||
DirectChat c -> case (chatDir, deletedContent) of
|
||||
(CIDirectRcv, CIRcvMsgContent mc) -> viewReceivedMessage (ttyFromContactDeleted c deletedText_) [] mc ts meta
|
||||
(CIDirectRcv, CIRcvMsgContent mc) -> viewReceivedMessage (ttyFromContactDeleted c deletedText_) [] mc ts tz meta
|
||||
_ -> prohibited
|
||||
GroupChat g -> case ciMsgContent deletedContent of
|
||||
Just mc ->
|
||||
let m = chatItemMember g ci
|
||||
in viewReceivedMessage (ttyFromGroupDeleted g m deletedText_) [] mc ts meta
|
||||
in viewReceivedMessage (ttyFromGroupDeleted g m deletedText_) [] mc ts tz meta
|
||||
_ -> prohibited
|
||||
_ -> prohibited
|
||||
where
|
||||
@@ -534,7 +535,7 @@ viewItemReaction showReactions chat CIReaction {chatDir, chatItem = CChatItem md
|
||||
(_, CIGroupSnd) -> [sentText]
|
||||
where
|
||||
view from msg
|
||||
| showReactions = viewReceivedReaction from msg reactionText ts $ utcToZonedTime tz sentAt
|
||||
| showReactions = viewReceivedReaction from msg reactionText ts tz sentAt
|
||||
| otherwise = []
|
||||
reactionText = plain $ (if added then "+ " else "- ") <> [emoji]
|
||||
emoji = case reaction of
|
||||
@@ -577,11 +578,11 @@ msgPreview = msgPlain . preview . msgContentText
|
||||
| T.length t <= 120 = t
|
||||
| otherwise = T.take 120 t <> "..."
|
||||
|
||||
viewRcvIntegrityError :: StyledString -> MsgErrorType -> CurrentTime -> CIMeta c 'MDRcv -> [StyledString]
|
||||
viewRcvIntegrityError from msgErr ts meta = receivedWithTime_ ts from [] meta (viewMsgIntegrityError msgErr) False
|
||||
viewRcvIntegrityError :: StyledString -> MsgErrorType -> CurrentTime -> TimeZone -> CIMeta c 'MDRcv -> [StyledString]
|
||||
viewRcvIntegrityError from msgErr ts tz meta = receivedWithTime_ ts tz from [] meta (viewMsgIntegrityError msgErr) False
|
||||
|
||||
viewRcvDecryptionError :: StyledString -> MsgDecryptError -> Word32 -> CurrentTime -> CIMeta c 'MDRcv -> [StyledString]
|
||||
viewRcvDecryptionError from err n ts meta = receivedWithTime_ ts from [] meta [ttyError $ msgDecryptErrorText err n] False
|
||||
viewRcvDecryptionError :: StyledString -> MsgDecryptError -> Word32 -> CurrentTime -> TimeZone -> CIMeta c 'MDRcv -> [StyledString]
|
||||
viewRcvDecryptionError from err n ts tz meta = receivedWithTime_ ts tz from [] meta [ttyError $ msgDecryptErrorText err n] False
|
||||
|
||||
viewMsgIntegrityError :: MsgErrorType -> [StyledString]
|
||||
viewMsgIntegrityError err = [ttyError $ msgIntegrityError err]
|
||||
@@ -1079,22 +1080,22 @@ viewContactUpdated
|
||||
where
|
||||
fullNameUpdate = if T.null fullName' || fullName' == n' then " removed full name" else " updated full name: " <> plain fullName'
|
||||
|
||||
viewReceivedMessage :: StyledString -> [StyledString] -> MsgContent -> CurrentTime -> CIMeta c d -> [StyledString]
|
||||
viewReceivedMessage :: StyledString -> [StyledString] -> MsgContent -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString]
|
||||
viewReceivedMessage = viewReceivedMessage_ False
|
||||
|
||||
viewReceivedUpdatedMessage :: StyledString -> [StyledString] -> MsgContent -> CurrentTime -> CIMeta c d -> [StyledString]
|
||||
viewReceivedUpdatedMessage :: StyledString -> [StyledString] -> MsgContent -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString]
|
||||
viewReceivedUpdatedMessage = viewReceivedMessage_ True
|
||||
|
||||
viewReceivedMessage_ :: Bool -> StyledString -> [StyledString] -> MsgContent -> CurrentTime -> CIMeta c d -> [StyledString]
|
||||
viewReceivedMessage_ updated from quote mc ts meta = receivedWithTime_ ts from quote meta (ttyMsgContent mc) updated
|
||||
viewReceivedMessage_ :: Bool -> StyledString -> [StyledString] -> MsgContent -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString]
|
||||
viewReceivedMessage_ updated from quote mc ts tz meta = receivedWithTime_ ts tz from quote meta (ttyMsgContent mc) updated
|
||||
|
||||
viewReceivedReaction :: StyledString -> [StyledString] -> StyledString -> CurrentTime -> ZonedTime -> [StyledString]
|
||||
viewReceivedReaction from styledMsg reactionText ts reactionTs =
|
||||
prependFirst (ttyMsgTime ts reactionTs <> " " <> from) (styledMsg <> [" " <> reactionText])
|
||||
viewReceivedReaction :: StyledString -> [StyledString] -> StyledString -> CurrentTime -> TimeZone -> UTCTime -> [StyledString]
|
||||
viewReceivedReaction from styledMsg reactionText ts tz reactionTs =
|
||||
prependFirst (ttyMsgTime ts tz reactionTs <> " " <> from) (styledMsg <> [" " <> reactionText])
|
||||
|
||||
receivedWithTime_ :: CurrentTime -> StyledString -> [StyledString] -> CIMeta c d -> [StyledString] -> Bool -> [StyledString]
|
||||
receivedWithTime_ ts from quote CIMeta {localItemTs, itemId, itemEdited, itemDeleted, itemLive} styledMsg updated = do
|
||||
prependFirst (ttyMsgTime ts localItemTs <> " " <> from) (quote <> prependFirst (indent <> live) styledMsg)
|
||||
receivedWithTime_ :: CurrentTime -> TimeZone -> StyledString -> [StyledString] -> CIMeta c d -> [StyledString] -> Bool -> [StyledString]
|
||||
receivedWithTime_ ts tz from quote CIMeta {itemId, itemTs, itemEdited, itemDeleted, itemLive} styledMsg updated = do
|
||||
prependFirst (ttyMsgTime ts tz itemTs <> " " <> from) (quote <> prependFirst (indent <> live) styledMsg)
|
||||
where
|
||||
indent = if null quote then "" else " "
|
||||
live
|
||||
@@ -1106,19 +1107,25 @@ receivedWithTime_ ts from quote CIMeta {localItemTs, itemId, itemEdited, itemDel
|
||||
Just False -> ttyFrom "[LIVE ended] "
|
||||
_ -> ""
|
||||
|
||||
ttyMsgTime :: CurrentTime -> ZonedTime -> StyledString
|
||||
ttyMsgTime ts t =
|
||||
let localTime = zonedTimeToLocalTime t
|
||||
tz = zonedTimeZone t
|
||||
fmt =
|
||||
if (localDay localTime < localDay (zonedTimeToLocalTime $ utcToZonedTime tz ts))
|
||||
&& (timeOfDayToTime (localTimeOfDay localTime) > (6 * 60 * 60 :: DiffTime))
|
||||
then "%m-%d" -- if message is from yesterday or before and 6 hours has passed since midnight
|
||||
else "%H:%M"
|
||||
ttyMsgTime :: CurrentTime -> TimeZone -> UTCTime -> StyledString
|
||||
ttyMsgTime now tz time =
|
||||
let fmt = if recent now tz time then "%H:%M" else "%m-%d"
|
||||
localTime = utcToLocalTime tz time
|
||||
in styleTime $ formatTime defaultTimeLocale fmt localTime
|
||||
|
||||
viewSentMessage :: StyledString -> [StyledString] -> MsgContent -> CurrentTime -> CIMeta c d -> [StyledString]
|
||||
viewSentMessage to quote mc ts meta@CIMeta {itemEdited, itemDeleted, itemLive} = sentWithTime_ ts (prependFirst to $ quote <> prependFirst (indent <> live) (ttyMsgContent mc)) meta
|
||||
recent :: CurrentTime -> TimeZone -> UTCTime -> Bool
|
||||
recent now tz time = do
|
||||
let localNow = utcToLocalTime tz now
|
||||
localNowDay = localDay localNow
|
||||
localTime = utcToLocalTime tz time
|
||||
localTimeDay = localDay localTime
|
||||
previousDay18 = LocalTime (addDays (-1) localNowDay) (TimeOfDay 18 0 0)
|
||||
currentDay12 = LocalTime localNowDay (TimeOfDay 12 0 0)
|
||||
localNowDay == localTimeDay
|
||||
|| (localNow < currentDay12 && localTime >= previousDay18 && localTimeDay < localNowDay)
|
||||
|
||||
viewSentMessage :: StyledString -> [StyledString] -> MsgContent -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString]
|
||||
viewSentMessage to quote mc ts tz meta@CIMeta {itemEdited, itemDeleted, itemLive} = sentWithTime_ ts tz (prependFirst to $ quote <> prependFirst (indent <> live) (ttyMsgContent mc)) meta
|
||||
where
|
||||
indent = if null quote then "" else " "
|
||||
live
|
||||
@@ -1128,12 +1135,12 @@ viewSentMessage to quote mc ts meta@CIMeta {itemEdited, itemDeleted, itemLive} =
|
||||
Just False -> ttyTo "[LIVE] "
|
||||
_ -> ""
|
||||
|
||||
viewSentBroadcast :: MsgContent -> Int -> CurrentTime -> ZonedTime -> [StyledString]
|
||||
viewSentBroadcast mc n ts t = prependFirst (highlight' "/feed" <> " (" <> sShow n <> ") " <> ttyMsgTime ts t <> " ") (ttyMsgContent mc)
|
||||
viewSentBroadcast :: MsgContent -> Int -> CurrentTime -> TimeZone -> UTCTime -> [StyledString]
|
||||
viewSentBroadcast mc n ts tz time = prependFirst (highlight' "/feed" <> " (" <> sShow n <> ") " <> ttyMsgTime ts tz time <> " ") (ttyMsgContent mc)
|
||||
|
||||
viewSentFileInvitation :: StyledString -> CIFile d -> CurrentTime -> CIMeta c d -> [StyledString]
|
||||
viewSentFileInvitation to CIFile {fileId, filePath, fileStatus} ts = case filePath of
|
||||
Just fPath -> sentWithTime_ ts $ ttySentFile fPath
|
||||
viewSentFileInvitation :: StyledString -> CIFile d -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString]
|
||||
viewSentFileInvitation to CIFile {fileId, filePath, fileStatus} ts tz = case filePath of
|
||||
Just fPath -> sentWithTime_ ts tz $ ttySentFile fPath
|
||||
_ -> const []
|
||||
where
|
||||
ttySentFile fPath = ["/f " <> to <> ttyFilePath fPath] <> cancelSending
|
||||
@@ -1141,9 +1148,9 @@ viewSentFileInvitation to CIFile {fileId, filePath, fileStatus} ts = case filePa
|
||||
CIFSSndTransfer _ _ -> []
|
||||
_ -> ["use " <> highlight ("/fc " <> show fileId) <> " to cancel sending"]
|
||||
|
||||
sentWithTime_ :: CurrentTime -> [StyledString] -> CIMeta c d -> [StyledString]
|
||||
sentWithTime_ ts styledMsg CIMeta {localItemTs} =
|
||||
prependFirst (ttyMsgTime ts localItemTs <> " ") styledMsg
|
||||
sentWithTime_ :: CurrentTime -> TimeZone -> [StyledString] -> CIMeta c d -> [StyledString]
|
||||
sentWithTime_ ts tz styledMsg CIMeta {itemTs} =
|
||||
prependFirst (ttyMsgTime ts tz itemTs <> " ") styledMsg
|
||||
|
||||
ttyMsgContent :: MsgContent -> [StyledString]
|
||||
ttyMsgContent = msgPlain . msgContentText
|
||||
@@ -1179,8 +1186,8 @@ uploadingFile status _ = [status <> " uploading file"] -- shouldn't happen
|
||||
sndFile :: SndFileTransfer -> StyledString
|
||||
sndFile SndFileTransfer {fileId, fileName} = fileTransferStr fileId fileName
|
||||
|
||||
viewReceivedFileInvitation :: StyledString -> CIFile d -> CurrentTime -> CIMeta c d -> [StyledString]
|
||||
viewReceivedFileInvitation from file ts meta = receivedWithTime_ ts from [] meta (receivedFileInvitation_ file) False
|
||||
viewReceivedFileInvitation :: StyledString -> CIFile d -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString]
|
||||
viewReceivedFileInvitation from file ts tz meta = receivedWithTime_ ts tz from [] meta (receivedFileInvitation_ file) False
|
||||
|
||||
receivedFileInvitation_ :: CIFile d -> [StyledString]
|
||||
receivedFileInvitation_ CIFile {fileId, fileName, fileSize, fileStatus} =
|
||||
|
||||
+38
-20
@@ -521,8 +521,9 @@ testGetSetSMPServers =
|
||||
alice #$> ("/smp", id, "smp://1234-w==@smp1.example.im")
|
||||
alice #$> ("/smp smp://1234-w==:password@smp1.example.im", id, "ok")
|
||||
alice #$> ("/smp", id, "smp://1234-w==:password@smp1.example.im")
|
||||
alice #$> ("/smp smp://2345-w==@smp2.example.im;smp://3456-w==@smp3.example.im:5224", id, "ok")
|
||||
alice #$> ("/smp", id, "smp://2345-w==@smp2.example.im")
|
||||
alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok")
|
||||
alice ##> "/smp"
|
||||
alice <## "smp://2345-w==@smp2.example.im"
|
||||
alice <## "smp://3456-w==@smp3.example.im:5224"
|
||||
alice #$> ("/smp default", id, "ok")
|
||||
alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001")
|
||||
@@ -551,8 +552,9 @@ testGetSetXFTPServers =
|
||||
alice #$> ("/xftp", id, "xftp://1234-w==@xftp1.example.im")
|
||||
alice #$> ("/xftp xftp://1234-w==:password@xftp1.example.im", id, "ok")
|
||||
alice #$> ("/xftp", id, "xftp://1234-w==:password@xftp1.example.im")
|
||||
alice #$> ("/xftp xftp://2345-w==@xftp2.example.im;xftp://3456-w==@xftp3.example.im:5224", id, "ok")
|
||||
alice #$> ("/xftp", id, "xftp://2345-w==@xftp2.example.im")
|
||||
alice #$> ("/xftp xftp://2345-w==@xftp2.example.im xftp://3456-w==@xftp3.example.im:5224", id, "ok")
|
||||
alice ##> "/xftp"
|
||||
alice <## "xftp://2345-w==@xftp2.example.im"
|
||||
alice <## "xftp://3456-w==@xftp3.example.im:5224"
|
||||
alice #$> ("/xftp default", id, "ok")
|
||||
alice #$> ("/xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002")
|
||||
@@ -1135,39 +1137,55 @@ testCreateUserDefaultServers :: HasCallStack => FilePath -> IO ()
|
||||
testCreateUserDefaultServers =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice _ -> do
|
||||
alice #$> ("/smp smp://2345-w==@smp2.example.im;smp://3456-w==@smp3.example.im:5224", id, "ok")
|
||||
alice #$> ("/smp", id, "smp://2345-w==@smp2.example.im")
|
||||
alice <## "smp://3456-w==@smp3.example.im:5224"
|
||||
alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok")
|
||||
alice #$> ("/xftp xftp://2345-w==@xftp2.example.im xftp://3456-w==@xftp3.example.im:5224", id, "ok")
|
||||
checkCustomServers alice
|
||||
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
|
||||
alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001")
|
||||
alice #$> ("/xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002")
|
||||
|
||||
-- with same_smp=off
|
||||
-- with same_servers=off
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
alice #$> ("/smp", id, "smp://2345-w==@smp2.example.im")
|
||||
alice <## "smp://3456-w==@smp3.example.im:5224"
|
||||
checkCustomServers alice
|
||||
|
||||
alice ##> "/create user same_smp=off alisa2"
|
||||
alice ##> "/create user same_servers=off alisa2"
|
||||
showActiveUser alice "alisa2"
|
||||
|
||||
alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001")
|
||||
alice #$> ("/xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002")
|
||||
where
|
||||
checkCustomServers alice = do
|
||||
alice ##> "/smp"
|
||||
alice <## "smp://2345-w==@smp2.example.im"
|
||||
alice <## "smp://3456-w==@smp3.example.im:5224"
|
||||
alice ##> "/xftp"
|
||||
alice <## "xftp://2345-w==@xftp2.example.im"
|
||||
alice <## "xftp://3456-w==@xftp3.example.im:5224"
|
||||
|
||||
testCreateUserSameServers :: HasCallStack => FilePath -> IO ()
|
||||
testCreateUserSameServers =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice _ -> do
|
||||
alice #$> ("/smp smp://2345-w==@smp2.example.im;smp://3456-w==@smp3.example.im:5224", id, "ok")
|
||||
alice #$> ("/smp", id, "smp://2345-w==@smp2.example.im")
|
||||
alice <## "smp://3456-w==@smp3.example.im:5224"
|
||||
alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok")
|
||||
alice #$> ("/xftp xftp://2345-w==@xftp2.example.im xftp://3456-w==@xftp3.example.im:5224", id, "ok")
|
||||
checkCustomServers alice
|
||||
|
||||
alice ##> "/create user same_smp=on alisa"
|
||||
alice ##> "/create user same_servers=on alisa"
|
||||
showActiveUser alice "alisa"
|
||||
|
||||
alice #$> ("/smp", id, "smp://2345-w==@smp2.example.im")
|
||||
checkCustomServers alice
|
||||
where
|
||||
checkCustomServers alice = do
|
||||
alice ##> "/smp"
|
||||
alice <## "smp://2345-w==@smp2.example.im"
|
||||
alice <## "smp://3456-w==@smp3.example.im:5224"
|
||||
alice ##> "/xftp"
|
||||
alice <## "xftp://2345-w==@xftp2.example.im"
|
||||
alice <## "xftp://3456-w==@xftp3.example.im:5224"
|
||||
|
||||
testDeleteUser :: HasCallStack => FilePath -> IO ()
|
||||
testDeleteUser =
|
||||
@@ -1309,7 +1327,7 @@ testUsersDifferentCIExpirationTTL tmp = do
|
||||
|
||||
alice #$> ("/_get chat @4 count=100", chat, [])
|
||||
where
|
||||
cfg = testCfg {ciExpirationInterval = 500000}
|
||||
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000}
|
||||
|
||||
testUsersRestartCIExpiration :: HasCallStack => FilePath -> IO ()
|
||||
testUsersRestartCIExpiration tmp = do
|
||||
@@ -1392,7 +1410,7 @@ testUsersRestartCIExpiration tmp = do
|
||||
|
||||
alice #$> ("/_get chat @4 count=100", chat, [])
|
||||
where
|
||||
cfg = testCfg {ciExpirationInterval = 500000}
|
||||
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000}
|
||||
|
||||
testEnableCIExpirationOnlyForOneUser :: HasCallStack => FilePath -> IO ()
|
||||
testEnableCIExpirationOnlyForOneUser tmp = do
|
||||
@@ -1463,7 +1481,7 @@ testEnableCIExpirationOnlyForOneUser tmp = do
|
||||
-- new messages are not deleted for second user
|
||||
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4"), (1, "alisa 5"), (0, "alisa 6")])
|
||||
where
|
||||
cfg = testCfg {ciExpirationInterval = 500000}
|
||||
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000}
|
||||
|
||||
testDisableCIExpirationOnlyForOneUser :: HasCallStack => FilePath -> IO ()
|
||||
testDisableCIExpirationOnlyForOneUser tmp = do
|
||||
@@ -1521,7 +1539,7 @@ testDisableCIExpirationOnlyForOneUser tmp = do
|
||||
-- second user messages are deleted
|
||||
alice #$> ("/_get chat @4 count=100", chat, [])
|
||||
where
|
||||
cfg = testCfg {ciExpirationInterval = 500000}
|
||||
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000}
|
||||
|
||||
testUsersTimedMessages :: HasCallStack => FilePath -> IO ()
|
||||
testUsersTimedMessages tmp = do
|
||||
|
||||
@@ -9,6 +9,7 @@ import Control.Concurrent (threadDelay)
|
||||
import Control.Concurrent.Async (concurrently_)
|
||||
import Control.Monad (when)
|
||||
import qualified Data.Text as T
|
||||
import Simplex.Chat.Controller (ChatConfig (..))
|
||||
import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
|
||||
import Simplex.Chat.Types (GroupMemberRole (..))
|
||||
import System.Directory (copyFile)
|
||||
@@ -420,7 +421,7 @@ testGroup2 =
|
||||
|
||||
testGroupDelete :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDelete =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
testChatCfg3 cfg aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
alice ##> "/d #team"
|
||||
@@ -444,12 +445,15 @@ testGroupDelete =
|
||||
alice <##> bob
|
||||
alice <##> cath
|
||||
-- unused group contacts are deleted
|
||||
threadDelay 3000000
|
||||
bob ##> "@cath hi"
|
||||
bob <## "no contact cath"
|
||||
(cath </)
|
||||
cath ##> "@bob hi"
|
||||
cath <## "no contact bob"
|
||||
(bob </)
|
||||
where
|
||||
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
|
||||
|
||||
testGroupSameName :: HasCallStack => FilePath -> IO ()
|
||||
testGroupSameName =
|
||||
@@ -1151,7 +1155,7 @@ testUpdateMemberRole =
|
||||
|
||||
testGroupDeleteUnusedContacts :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDeleteUnusedContacts =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
testChatCfg3 cfg aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
-- create group 1
|
||||
createGroup3 "team" alice bob cath
|
||||
@@ -1210,6 +1214,7 @@ testGroupDeleteUnusedContacts =
|
||||
cath `hasContactProfiles` ["alice", "bob", "cath"]
|
||||
-- delete group 2, unused contacts and profiles are deleted
|
||||
deleteGroup alice bob cath "club"
|
||||
threadDelay 3000000
|
||||
bob ##> "/contacts"
|
||||
bob <## "alice (Alice)"
|
||||
bob `hasContactProfiles` ["alice", "bob"]
|
||||
@@ -1217,6 +1222,7 @@ testGroupDeleteUnusedContacts =
|
||||
cath <## "alice (Alice)"
|
||||
cath `hasContactProfiles` ["alice", "cath"]
|
||||
where
|
||||
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
|
||||
deleteGroup :: HasCallStack => TestCC -> TestCC -> TestCC -> String -> IO ()
|
||||
deleteGroup alice bob cath group = do
|
||||
alice ##> ("/d #" <> group)
|
||||
@@ -1827,7 +1833,7 @@ testGroupLinkIncognitoMembership =
|
||||
|
||||
testGroupLinkUnusedHostContactDeleted :: HasCallStack => FilePath -> IO ()
|
||||
testGroupLinkUnusedHostContactDeleted =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
testChatCfg2 cfg aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
-- create group 1
|
||||
alice ##> "/g team"
|
||||
@@ -1881,10 +1887,12 @@ testGroupLinkUnusedHostContactDeleted =
|
||||
bob `hasContactProfiles` ["alice", "bob"]
|
||||
-- delete group 2, unused host contact and profile are deleted
|
||||
bobLeaveDeleteGroup alice bob "club"
|
||||
threadDelay 3000000
|
||||
bob ##> "/contacts"
|
||||
(bob </)
|
||||
bob `hasContactProfiles` ["bob"]
|
||||
where
|
||||
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
|
||||
bobLeaveDeleteGroup :: HasCallStack => TestCC -> TestCC -> String -> IO ()
|
||||
bobLeaveDeleteGroup alice bob group = do
|
||||
bob ##> ("/l " <> group)
|
||||
@@ -1899,7 +1907,7 @@ testGroupLinkUnusedHostContactDeleted =
|
||||
|
||||
testGroupLinkIncognitoUnusedHostContactsDeleted :: HasCallStack => FilePath -> IO ()
|
||||
testGroupLinkIncognitoUnusedHostContactsDeleted =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
testChatCfg2 cfg aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
bob #$> ("/incognito on", id, "ok")
|
||||
bobIncognitoTeam <- createGroupBobIncognito alice bob "team" "alice"
|
||||
@@ -1912,15 +1920,18 @@ testGroupLinkIncognitoUnusedHostContactsDeleted =
|
||||
bob `hasContactProfiles` ["alice", "alice", "bob", T.pack bobIncognitoTeam, T.pack bobIncognitoClub]
|
||||
-- delete group 1, unused host contact and profile are deleted
|
||||
bobLeaveDeleteGroup alice bob "team" bobIncognitoTeam
|
||||
threadDelay 3000000
|
||||
bob ##> "/contacts"
|
||||
bob <## "i alice_1 (Alice)"
|
||||
bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognitoClub]
|
||||
-- delete group 2, unused host contact and profile are deleted
|
||||
bobLeaveDeleteGroup alice bob "club" bobIncognitoClub
|
||||
threadDelay 3000000
|
||||
bob ##> "/contacts"
|
||||
(bob </)
|
||||
bob `hasContactProfiles` ["bob"]
|
||||
where
|
||||
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
|
||||
createGroupBobIncognito :: HasCallStack => TestCC -> TestCC -> String -> String -> IO String
|
||||
createGroupBobIncognito alice bob group bobsAliceContact = do
|
||||
alice ##> ("/g " <> group)
|
||||
|
||||
+6
-1
@@ -65,7 +65,12 @@ testSchemaMigrations = withTmpFiles $ do
|
||||
schema''' `shouldBe` schema'
|
||||
|
||||
skipComparisonForDownMigrations :: [String]
|
||||
skipComparisonForDownMigrations = ["20230504_recreate_msg_delivery_events_cleanup_messages"]
|
||||
skipComparisonForDownMigrations =
|
||||
[ -- on down migration msg_delivery_events table moves down to the end of the file
|
||||
"20230504_recreate_msg_delivery_events_cleanup_messages",
|
||||
-- on down migration idx_chat_items_timed_delete_at index moves down to the end of the file
|
||||
"20230529_indexes"
|
||||
]
|
||||
|
||||
getSchema :: FilePath -> FilePath -> IO String
|
||||
getSchema dpPath schemaPath = do
|
||||
|
||||
@@ -8,6 +8,7 @@ import ProtocolTests
|
||||
import SchemaDump
|
||||
import Test.Hspec
|
||||
import UnliftIO.Temporary (withTempDirectory)
|
||||
import ViewTests
|
||||
import WebRTCTests
|
||||
|
||||
main :: IO ()
|
||||
@@ -15,6 +16,7 @@ main = do
|
||||
setLogLevel LogError -- LogDebug
|
||||
withGlobalLogging logCfg . hspec $ do
|
||||
describe "SimpleX chat markdown" markdownTests
|
||||
describe "SimpleX chat view" viewTests
|
||||
describe "SimpleX chat protocol" protocolTests
|
||||
describe "WebRTC encryption" webRTCTests
|
||||
describe "Schema dump" schemaDumpTest
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module ViewTests where
|
||||
|
||||
import Data.Time
|
||||
import Simplex.Chat.View
|
||||
import Test.Hspec
|
||||
|
||||
viewTests :: Spec
|
||||
viewTests = do
|
||||
testRecent
|
||||
|
||||
testRecent :: Spec
|
||||
testRecent = describe "recent" $ do
|
||||
let tz = hoursToTimeZone 1
|
||||
now1159 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 10 * 3600 + 59 * 60) -- 11:59 in tz
|
||||
now1200 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 11 * 3600) -- 12:00 in tz
|
||||
today0000 = UTCTime (fromGregorian 2023 6 6) (secondsToDiffTime $ 23 * 3600) -- 00:00 in tz
|
||||
today0600 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 5 * 3600) -- 06:00 in tz
|
||||
today1200 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 11 * 3600) -- 12:00 in tz
|
||||
today1800 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 17 * 3600) -- 18:00 in tz
|
||||
today2359 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 22 * 3600 + 59 * 60) -- 23:59 in tz
|
||||
yesterday0000 = UTCTime (fromGregorian 2023 6 5) (secondsToDiffTime $ 23 * 3600) -- 00:00 in tz
|
||||
yesterday1759 = UTCTime (fromGregorian 2023 6 6) (secondsToDiffTime $ 16 * 3600 + 59 * 60) -- 17:59 in tz
|
||||
yesterday1800 = UTCTime (fromGregorian 2023 6 6) (secondsToDiffTime $ 17 * 3600) -- 18:00 in tz
|
||||
yesterday2359 = UTCTime (fromGregorian 2023 6 6) (secondsToDiffTime $ 22 * 3600 + 59 * 60) -- 23:59 in tz
|
||||
sameDayLastMonth1900 = UTCTime (fromGregorian 2023 5 7) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
prevDayLastMonth1900 = UTCTime (fromGregorian 2023 5 6) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
sameDayLastYear1900 = UTCTime (fromGregorian 2022 6 7) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
prevDayLastYear1900 = UTCTime (fromGregorian 2022 6 6) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
tomorrow0000 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 23 * 3600) -- 00:00 in tz
|
||||
tomorrow1759 = UTCTime (fromGregorian 2023 6 8) (secondsToDiffTime $ 16 * 3600 + 59 * 60) -- 17:59 in tz
|
||||
tomorrow1800 = UTCTime (fromGregorian 2023 6 8) (secondsToDiffTime $ 17 * 3600) -- 18:00 in tz
|
||||
tomorrow2359 = UTCTime (fromGregorian 2023 6 8) (secondsToDiffTime $ 22 * 3600 + 59 * 60) -- 23:59 in tz
|
||||
sameDayNextMonth1900 = UTCTime (fromGregorian 2023 7 7) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
prevDayNextMonth1900 = UTCTime (fromGregorian 2023 7 6) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
sameDayNextYear1900 = UTCTime (fromGregorian 2024 6 7) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
prevDayNextYear1900 = UTCTime (fromGregorian 2024 6 6) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
test tz now1159 today0000 True
|
||||
test tz now1159 today0600 True
|
||||
test tz now1159 today1200 True
|
||||
test tz now1159 today1800 True
|
||||
test tz now1159 today2359 True
|
||||
test tz now1159 yesterday0000 False
|
||||
test tz now1159 yesterday1759 False
|
||||
test tz now1159 yesterday1800 True
|
||||
test tz now1159 yesterday2359 True
|
||||
test tz now1159 sameDayLastMonth1900 False
|
||||
test tz now1159 prevDayLastMonth1900 False
|
||||
test tz now1159 sameDayLastYear1900 False
|
||||
test tz now1159 prevDayLastYear1900 False
|
||||
test tz now1159 tomorrow0000 False
|
||||
test tz now1159 tomorrow1759 False
|
||||
test tz now1159 tomorrow1800 False
|
||||
test tz now1159 tomorrow2359 False
|
||||
test tz now1159 sameDayNextMonth1900 False
|
||||
test tz now1159 prevDayNextMonth1900 False
|
||||
test tz now1159 sameDayNextYear1900 False
|
||||
test tz now1159 prevDayNextYear1900 False
|
||||
|
||||
test tz now1200 today0000 True
|
||||
test tz now1200 today0600 True
|
||||
test tz now1200 today1200 True
|
||||
test tz now1200 today1800 True
|
||||
test tz now1200 today2359 True
|
||||
test tz now1200 yesterday0000 False
|
||||
test tz now1200 yesterday1759 False
|
||||
test tz now1200 yesterday1800 False
|
||||
test tz now1200 yesterday2359 False
|
||||
test tz now1200 sameDayLastMonth1900 False
|
||||
test tz now1200 prevDayLastMonth1900 False
|
||||
test tz now1200 sameDayLastYear1900 False
|
||||
test tz now1200 prevDayLastYear1900 False
|
||||
test tz now1200 tomorrow0000 False
|
||||
test tz now1200 tomorrow1759 False
|
||||
test tz now1200 tomorrow1800 False
|
||||
test tz now1200 tomorrow2359 False
|
||||
test tz now1200 sameDayNextMonth1900 False
|
||||
test tz now1200 prevDayNextMonth1900 False
|
||||
test tz now1200 sameDayNextYear1900 False
|
||||
test tz now1200 prevDayNextYear1900 False
|
||||
where
|
||||
test tz now time expected =
|
||||
it ("returns " <> show expected <> " for time " <> show time <> " when time zone is " <> show tz <> " and current time is " <> show now) $
|
||||
recent now tz time `shouldBe` expected
|
||||
+139
-6
@@ -3,10 +3,47 @@ const markdownItAnchor = require("markdown-it-anchor")
|
||||
const markdownItReplaceLink = require('markdown-it-replace-link')
|
||||
const slugify = require("slugify")
|
||||
const uri = require('fast-uri')
|
||||
const i18n = require('eleventy-plugin-i18n');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const pluginRss = require('@11ty/eleventy-plugin-rss');
|
||||
const i18n = require('eleventy-plugin-i18n')
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const pluginRss = require('@11ty/eleventy-plugin-rss')
|
||||
const { JSDOM } = require('jsdom')
|
||||
|
||||
|
||||
// The implementation of Glossary feature
|
||||
const md = new markdownIt()
|
||||
const glossaryMarkdownContent = fs.readFileSync(path.resolve(__dirname, '../docs/GLOSSARY.md'), 'utf8')
|
||||
const glossaryHtmlContent = md.render(glossaryMarkdownContent)
|
||||
const glossaryDOM = new JSDOM(glossaryHtmlContent)
|
||||
const glossaryDocument = glossaryDOM.window.document
|
||||
const glossary = require('./src/_data/glossary.json')
|
||||
|
||||
glossary.forEach(item => {
|
||||
const headers = Array.from(glossaryDocument.querySelectorAll("h2"))
|
||||
const matchingHeader = headers.find(header => header.textContent.trim() === item.definition)
|
||||
|
||||
if (matchingHeader) {
|
||||
let sibling = matchingHeader.nextElementSibling
|
||||
let definition = ''
|
||||
let firstParagraph = ''
|
||||
let paragraphCount = 0
|
||||
|
||||
while (sibling && sibling.tagName !== 'H2') {
|
||||
if (sibling.tagName === 'P') {
|
||||
paragraphCount += 1
|
||||
if (firstParagraph === '') {
|
||||
firstParagraph = sibling.innerHTML
|
||||
}
|
||||
}
|
||||
definition += sibling.outerHTML || sibling.textContent
|
||||
sibling = sibling.nextElementSibling
|
||||
}
|
||||
|
||||
item.definition = definition
|
||||
item.tooltip = firstParagraph
|
||||
item.hasMultipleParagraphs = paragraphCount > 1
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const globalConfig = {
|
||||
@@ -55,6 +92,102 @@ module.exports = function (ty) {
|
||||
}
|
||||
})
|
||||
|
||||
ty.addFilter('applyGlossary', function (content) {
|
||||
const dom = new JSDOM(content)
|
||||
const { document } = dom.window
|
||||
const body = document.querySelector('body')
|
||||
const allContentNodes = document.querySelectorAll('p, td, a, h1, h2, h3, h4')
|
||||
const overlayIds = []
|
||||
|
||||
glossary.forEach((term, index) => {
|
||||
let changeNoted = false
|
||||
const id = term.term.toLowerCase().replace(/\s/g, '-')
|
||||
|
||||
allContentNodes.forEach((node) => {
|
||||
const regex = new RegExp(`(?<![/#])\\b${term.term}\\b`, 'gi')
|
||||
const replacement = `<span data-glossary="tooltip-${id}" class="glossary-term">${term.term}</span>`
|
||||
const beforeContent = node.innerHTML
|
||||
node.innerHTML = node.innerHTML.replace(regex, replacement)
|
||||
if (beforeContent !== node.innerHTML && !changeNoted) {
|
||||
changeNoted = true
|
||||
}
|
||||
})
|
||||
|
||||
if (changeNoted) {
|
||||
const definitionTooltipDiv = document.createElement('div')
|
||||
definitionTooltipDiv.id = `tooltip-${id}`
|
||||
definitionTooltipDiv.className = "glossary-tooltip"
|
||||
const titleH4 = document.createElement('h4')
|
||||
titleH4.innerHTML = term.term
|
||||
titleH4.className = "tooltip-title"
|
||||
const p = document.createElement('p')
|
||||
p.innerHTML = term.tooltip
|
||||
const innerDiv = document.createElement('div')
|
||||
innerDiv.appendChild(titleH4)
|
||||
innerDiv.appendChild(p)
|
||||
if (term.hasMultipleParagraphs) {
|
||||
const readMoreBtn = document.createElement('button')
|
||||
readMoreBtn.innerHTML = "Read more"
|
||||
readMoreBtn.className = "read-more-btn open-overlay-btn"
|
||||
readMoreBtn.setAttribute('data-show-overlay', id)
|
||||
innerDiv.appendChild(readMoreBtn)
|
||||
}
|
||||
innerDiv.className = "tooltip-content"
|
||||
definitionTooltipDiv.appendChild(innerDiv)
|
||||
body.appendChild(definitionTooltipDiv)
|
||||
}
|
||||
|
||||
let tooltipDom = new JSDOM(term.definition)
|
||||
let tooltipDocument = tooltipDom.window.document
|
||||
const hashList = [term.term.toLowerCase().replace(/\s/g, '-')]
|
||||
tooltipDocument.querySelectorAll('a[href*="#"]').forEach(a => {
|
||||
let hashIndex = a.href.indexOf("#")
|
||||
if (hashIndex !== -1) {
|
||||
let hash = a.href.substring(hashIndex + 1)
|
||||
hashList.push(hash)
|
||||
}
|
||||
})
|
||||
|
||||
hashList.forEach(hash => {
|
||||
if (!overlayIds.includes(hash)) {
|
||||
let termFromHash = glossary.find(term => term.term.toLowerCase().replace(/\s/g, '-') === hash)
|
||||
if (!termFromHash) return
|
||||
|
||||
const overlayDiv = document.createElement('div')
|
||||
overlayDiv.id = hash
|
||||
overlayDiv.className = "overlay glossary-overlay hidden"
|
||||
const overlayCardDiv = document.createElement('div')
|
||||
overlayCardDiv.className = "overlay-card"
|
||||
const overlayTitleH1 = document.createElement('h1')
|
||||
overlayTitleH1.className = "overlay-title"
|
||||
overlayTitleH1.innerHTML = termFromHash.term
|
||||
const overlayContent = document.createElement('div')
|
||||
overlayContent.className = "overlay-content"
|
||||
overlayContent.innerHTML = termFromHash.definition
|
||||
const crossSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg")
|
||||
crossSVG.setAttribute('class', 'close-overlay-btn')
|
||||
crossSVG.setAttribute('id', 'cross')
|
||||
crossSVG.setAttribute('width', '16')
|
||||
crossSVG.setAttribute('height', '16')
|
||||
crossSVG.setAttribute('viewBox', '0 0 13 13')
|
||||
crossSVG.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
|
||||
const crossPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
|
||||
crossPath.setAttribute('d', 'M12.7973 11.5525L7.59762 6.49833L12.7947 1.44675C13.055 1.19371 13.0658 0.771991 12.8188 0.505331C12.5718 0.238674 12.1602 0.227644 11.8999 0.480681L6.65343 5.58028L1.09979 0.182228C0.805 0.002228 0.430001 0.002228 0.135211 0.182228C-0.159579 0.362228 -0.159579 0.697228 0.135211 0.877228L5.68885 6.27528L0.4918 11.3295C0.231501 11.5825 0.220703 12.0042 0.467664 12.2709C0.714625 12.5376 1.12625 12.5486 1.38655 12.2956L6.63302 7.196L12.1867 12.5941C12.4815 12.7741 12.8565 12.7741 13.1513 12.5941C13.4461 12.4141 13.4461 12.0791 13.1513 11.8991L12.7973 11.5525Z')
|
||||
crossSVG.appendChild(crossPath)
|
||||
|
||||
overlayCardDiv.appendChild(overlayTitleH1)
|
||||
overlayCardDiv.appendChild(overlayContent)
|
||||
overlayCardDiv.appendChild(crossSVG)
|
||||
overlayDiv.appendChild(overlayCardDiv)
|
||||
body.appendChild(overlayDiv)
|
||||
overlayIds.push(hash)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return dom.serialize()
|
||||
})
|
||||
|
||||
ty.addShortcode("completeRoute", (obj) => {
|
||||
const urlParts = obj.url.split("/")
|
||||
|
||||
@@ -88,7 +221,7 @@ module.exports = function (ty) {
|
||||
}
|
||||
})
|
||||
|
||||
ty.addPlugin(pluginRss);
|
||||
ty.addPlugin(pluginRss)
|
||||
|
||||
ty.addPlugin(i18n, {
|
||||
translations,
|
||||
@@ -139,7 +272,7 @@ module.exports = function (ty) {
|
||||
const url = doc.url.replace("/docs/", "")
|
||||
const urlParts = url.split("/")
|
||||
|
||||
if (doc.inputPath.includes(referenceSubmenu)) {
|
||||
if (doc.inputPath.split('/').includes(referenceSubmenu)) {
|
||||
if (urlParts.length === 1 && urlParts[0] !== "") {
|
||||
const index = newDocs.findIndex((ele) => ele.lang === 'en' && ele.menu === referenceMenu.menu)
|
||||
if (index !== -1) {
|
||||
|
||||
@@ -231,5 +231,6 @@
|
||||
"click-to-see": "Click to see",
|
||||
"menu": "Menu",
|
||||
"on-this-page": "On this page",
|
||||
"back-to-top": "Back to top"
|
||||
"back-to-top": "Back to top",
|
||||
"glossary": "Glossary"
|
||||
}
|
||||
@@ -29,6 +29,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"eleventy-plugin-i18n": "^0.1.3",
|
||||
"gray-matter": "^4.0.3"
|
||||
"fs": "^0.0.1-security",
|
||||
"gray-matter": "^4.0.3",
|
||||
"jsdom": "^22.1.0",
|
||||
"markdown-it": "^13.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
[
|
||||
{
|
||||
"term": "Address portability",
|
||||
"definition": "Address portability"
|
||||
},
|
||||
{
|
||||
"term": "Anonymous credentials",
|
||||
"definition": "Anonymous credentials"
|
||||
},
|
||||
{
|
||||
"term": "Blockchain",
|
||||
"definition": "Blockchain"
|
||||
},
|
||||
{
|
||||
"term": "Break-in recovery",
|
||||
"definition": "Post-compromise security"
|
||||
},
|
||||
{
|
||||
"term": "Centralized network",
|
||||
"definition": "Centralized network"
|
||||
},
|
||||
{
|
||||
"term": "Content padding",
|
||||
"definition": "Message padding"
|
||||
},
|
||||
{
|
||||
"term": "Decentralized network",
|
||||
"definition": "Decentralized network"
|
||||
},
|
||||
{
|
||||
"term": "Defense in depth",
|
||||
"definition": "Defense in depth"
|
||||
},
|
||||
{
|
||||
"term": "Double ratchet algorithm",
|
||||
"definition": "Double ratchet algorithm"
|
||||
},
|
||||
{
|
||||
"term": "End-to-end encryption",
|
||||
"definition": "End-to-end encryption"
|
||||
},
|
||||
{
|
||||
"term": "Federated network",
|
||||
"definition": "Federated network"
|
||||
},
|
||||
{
|
||||
"term": "Forward secrecy",
|
||||
"definition": "Forward secrecy"
|
||||
},
|
||||
{
|
||||
"term": "Key agreement protocol",
|
||||
"definition": "Key agreement protocol"
|
||||
},
|
||||
{
|
||||
"term": "Key exchange",
|
||||
"definition": "Key agreement protocol"
|
||||
},
|
||||
{
|
||||
"term": "Man-in-the-middle attack",
|
||||
"definition": "Man-in-the-middle attack"
|
||||
},
|
||||
{
|
||||
"term": "Merkle directed acyclic graph",
|
||||
"definition": "Merkle directed acyclic graph"
|
||||
},
|
||||
{
|
||||
"term": "Message padding",
|
||||
"definition": "Message padding"
|
||||
},
|
||||
{
|
||||
"term": "Onion routing",
|
||||
"definition": "Onion routing"
|
||||
},
|
||||
{
|
||||
"term": "Overlay network",
|
||||
"definition": "Overlay network"
|
||||
},
|
||||
{
|
||||
"term": "Pairwise pseudonymous identifier",
|
||||
"definition": "Pairwise pseudonymous identifier"
|
||||
},
|
||||
{
|
||||
"term": "Peer-to-peer",
|
||||
"definition": "Peer-to-peer"
|
||||
},
|
||||
{
|
||||
"term": "Perfect forward secrecy",
|
||||
"definition": "Forward secrecy"
|
||||
},
|
||||
{
|
||||
"term": "Post-compromise security",
|
||||
"definition": "Post-compromise security"
|
||||
},
|
||||
{
|
||||
"term": "Post-quantum cryptography",
|
||||
"definition": "Post-quantum cryptography"
|
||||
},
|
||||
{
|
||||
"term": "Proxied peer-to-peer",
|
||||
"definition": "Proxied peer-to-peer"
|
||||
},
|
||||
{
|
||||
"term": "Recovery from compromise",
|
||||
"definition": "Post-compromise security"
|
||||
},
|
||||
{
|
||||
"term": "User identity",
|
||||
"definition": "User identity"
|
||||
}
|
||||
]
|
||||
@@ -22,7 +22,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<svg class="fill-grey-black dark:fill-white fixed right-5 top-5 cursor-pointer close-overlay-btn" id="cross" width="16" height="16" viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="close-overlay-btn" id="cross" width="16" height="16" viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.7973 11.5525L7.59762 6.49833L12.7947 1.44675C13.055 1.19371 13.0658 0.771991 12.8188 0.505331C12.5718 0.238674 12.1602 0.227644 11.8999 0.480681L6.65343 5.58028L1.09979 0.182228C0.839522 -0.070157 0.427909 -0.059127 0.18094 0.207531C-0.0660305 0.474191 -0.0552645 0.895911 0.205003 1.14894L5.70862 6.49833L0.20247 11.851C-0.0577975 12.104 -0.0685635 12.5257 0.178407 12.7924C0.306324 12.9306 0.477936 13 0.650181 13C0.811033 13 0.971873 12.9397 1.09726 12.817L6.65343 7.41639L11.9025 12.5186C12.0285 12.6406 12.1893 12.7015 12.3495 12.7015C12.5218 12.7015 12.6934 12.6321 12.8213 12.4939C13.0689 12.2273 13.0582 11.8062 12.7973 11.5525Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<section class="hidden xl:block h-screen pt-[66px] bg-white dark:bg-gradient-radial-mobile dark:lg:bg-gradient-radial">
|
||||
<div class="container m-auto h-full flex items-center justify-between px-5">
|
||||
<div class="flex flex-col items-start justify-center w-full h-full">
|
||||
<p class="text-[38px] leading-[43px] font-bold max-w-[500px] mb-[30px] primary-header-contact">{{ header | i18n({}, lang ) | safe }}</p>
|
||||
<p class="text-[20px] leading-[28px] text-[#606C71] dark:text-white font-bold max-w-[475px] mb-[80px] secondary-header-contact">{{ "contact-hero-subheader" | i18n({}, lang ) | safe }}</p>
|
||||
<h1 class="text-[38px] leading-[43px] font-bold max-w-[500px] mb-[30px] primary-header-contact">{{ header | i18n({}, lang ) | safe }}</h1>
|
||||
<h2 class="text-[20px] leading-[28px] text-[#606C71] dark:text-white font-bold max-w-[475px] mb-[80px] secondary-header-contact">{{ "contact-hero-subheader" | i18n({}, lang ) | safe }}</h2>
|
||||
<p class="text-grey-black dark:text-white text-base mb-[16px]">
|
||||
{{ "contact-hero-p-1" | i18n({}, lang ) | safe }}
|
||||
</p>
|
||||
@@ -43,7 +43,7 @@
|
||||
<section class="block xl:hidden pt-[106px] pb-[90px] bg-white dark:bg-gradient-radial-mobile dark:lg:bg-gradient-radial">
|
||||
<div class="container m-auto h-full px-5">
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-[28px] font-bold text-center max-w-[602px] mb-[40px] primary-header-contact">{{ header | i18n({}, lang ) | safe }}</p>
|
||||
<h1 class="text-[28px] font-bold text-center max-w-[602px] mb-[40px] primary-header-contact">{{ header | i18n({}, lang ) | safe }}</h1>
|
||||
<p class="text-[20px] leading-[28px] text-grey-black dark:text-white font-medium mb-[30px]">{{ "to-make-a-connection" | i18n({}, lang ) | safe }}</p>
|
||||
|
||||
<div class="flex flex-col justify-center items-center p-4 w-full max-w-[468px] min-h-[131px] rounded-[30px] border-[1px] border-[#A8B0B4] dark:border-white border-opacity-60 mb-6 relative">
|
||||
@@ -88,7 +88,7 @@
|
||||
<div class="hidden md:block xl:hidden for-tablet">
|
||||
<div class="contact-tab">
|
||||
<div class="flex items-center justify-between my-[40px] contact-tab-btn cursor-pointer">
|
||||
<p class="text-xl font-bold">{{ "scan-the-qr-code-with-the-simplex-chat-app" | i18n({}, lang ) | safe }}</p>
|
||||
<h2 class="text-xl font-bold">{{ "scan-the-qr-code-with-the-simplex-chat-app" | i18n({}, lang ) | safe }}</h2>
|
||||
<svg class="fill-grey-black dark:fill-white" width="10" height="5" viewBox="0 0 10 5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.40813 4.79332C8.69689 5.06889 9.16507 5.06889 9.45384 4.79332C9.7426 4.51775 9.7426 4.07097 9.45384 3.7954L5.69327 0.206676C5.65717 0.17223 5.61827 0.142089 5.57727 0.116255C5.29026 -0.064587 4.90023 -0.0344467 4.64756 0.206676L0.886983 3.7954C0.598219 4.07097 0.598219 4.51775 0.886983 4.79332C1.17575 5.06889 1.64393 5.06889 1.93269 4.79332L5.17041 1.70356L8.40813 4.79332Z"/>
|
||||
</svg>
|
||||
@@ -108,7 +108,7 @@
|
||||
<div class="hidden xl:block">
|
||||
<div class="contact-tab">
|
||||
<div class="flex items-center justify-between my-[40px] contact-tab-btn cursor-pointer">
|
||||
<p class="text-xl font-bold">{{ "installing-simplex-chat-to-terminal" | i18n({}, lang ) | safe }}</p>
|
||||
<h2 class="text-xl font-bold">{{ "installing-simplex-chat-to-terminal" | i18n({}, lang ) | safe }}</h2>
|
||||
<svg class="fill-grey-black dark:fill-white" width="10" height="5" viewBox="0 0 10 5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.40813 4.79332C8.69689 5.06889 9.16507 5.06889 9.45384 4.79332C9.7426 4.51775 9.7426 4.07097 9.45384 3.7954L5.69327 0.206676C5.65717 0.17223 5.61827 0.142089 5.57727 0.116255C5.29026 -0.064587 4.90023 -0.0344467 4.64756 0.206676L0.886983 3.7954C0.598219 4.07097 0.598219 4.51775 0.886983 4.79332C1.17575 5.06889 1.64393 5.06889 1.93269 4.79332L5.17041 1.70356L8.40813 4.79332Z"/>
|
||||
</svg>
|
||||
@@ -139,7 +139,7 @@
|
||||
|
||||
<div class="contact-tab">
|
||||
<div class="flex items-center justify-between my-[40px] contact-tab-btn cursor-pointer">
|
||||
<p class="text-xl font-bold">{{ "if-you-already-installed-simplex-chat-for-the-terminal" | i18n({}, lang ) | safe }}</p>
|
||||
<h2 class="text-xl font-bold">{{ "if-you-already-installed-simplex-chat-for-the-terminal" | i18n({}, lang ) | safe }}</h2>
|
||||
<svg class="fill-grey-black dark:fill-white" width="10" height="5" viewBox="0 0 10 5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.40813 4.79332C8.69689 5.06889 9.16507 5.06889 9.45384 4.79332C9.7426 4.51775 9.7426 4.07097 9.45384 3.7954L5.69327 0.206676C5.65717 0.17223 5.61827 0.142089 5.57727 0.116255C5.29026 -0.064587 4.90023 -0.0344467 4.64756 0.206676L0.886983 3.7954C0.598219 4.07097 0.598219 4.51775 0.886983 4.79332C1.17575 5.06889 1.64393 5.06889 1.93269 4.79332L5.17041 1.70356L8.40813 4.79332Z"/>
|
||||
</svg>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<div class="h-[40px] flex gap-4 justify-center">
|
||||
<a href="https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion">
|
||||
<svg class="fill-primary-light dark:fill-primary-dark" width="40" height="40" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.585 0C11.9025 0 0 11.9025 0 26.585C0 41.2674 11.9025 53.1699 26.585 53.1699C41.2674 53.1699 53.1699 41.2674 53.1699 26.585C53.1699 11.9025 41.2674 0 26.585 0ZM11.3862 17.3518L17.6366 23.4373L23.9313 17.3088L17.6787 11.2209L20.866 8.1179L27.1187 14.2061L33.4932 8L36.6199 11.044L30.2448 17.25L36.4982 23.3379L42.8733 17.1321L46 20.1761L39.6249 26.3818L45.8789 32.4702L42.6916 35.5732L36.4376 29.4848L30.0631 35.6906L36.3171 41.7791L33.1299 44.8821L26.8759 38.7936L20.5026 45L17.3759 41.956L23.8003 35.693L17.5493 29.6073L11.1255 35.8621L8 32.8194L14.4244 26.5646L8.17397 20.4792L11.3862 17.3518ZM27.0125 32.5656L33.311 26.4408L27.0576 20.3535L27.0568 20.3516L20.7615 26.4799L27.0125 32.5656Z" fill="#0053D0"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.585 0C11.9025 0 0 11.9025 0 26.585C0 41.2674 11.9025 53.1699 26.585 53.1699C41.2674 53.1699 53.1699 41.2674 53.1699 26.585C53.1699 11.9025 41.2674 0 26.585 0ZM11.3862 17.3518L17.6366 23.4373L23.9313 17.3088L17.6787 11.2209L20.866 8.1179L27.1187 14.2061L33.4932 8L36.6199 11.044L30.2448 17.25L36.4982 23.3379L42.8733 17.1321L46 20.1761L39.6249 26.3818L45.8789 32.4702L42.6916 35.5732L36.4376 29.4848L30.0631 35.6906L36.3171 41.7791L33.1299 44.8821L26.8759 38.7936L20.5026 45L17.3759 41.956L23.8003 35.693L17.5493 29.6073L11.1255 35.8621L8 32.8194L14.4244 26.5646L8.17397 20.4792L11.3862 17.3518ZM27.0125 32.5656L33.311 26.4408L27.0576 20.3535L27.0568 20.3516L20.7615 26.4799L27.0125 32.5656Z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://github.com/simplex-chat" target="_blank">
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<img class="static-phone-mobile md:hidden" src="/img/new/mobile-hero.png" alt="" />
|
||||
|
||||
<article class="w-full xl:max-w-[600px] landing-page-header-article">
|
||||
<p class="primary-header text-center xl:text-left xl:rtl:text-right font-bold text-[38px] md:text-[55px] leading-[46px] md:leading-[63px] mb-2 xl:mb-8">{{ "hero-header" | i18n({}, lang ) | safe }}</p>
|
||||
<p class="secondary-header text-center xl:text-left xl:rtl:text-right font-bold text-[28px] md:text-[38px] leading-[36px] md:leading-[43px] mb-2 xl:mb-8 tracking-[0.01em]">{{ "hero-subheader" | i18n({}, lang ) | safe }}</p>
|
||||
<h1 class="primary-header text-center xl:text-left xl:rtl:text-right font-bold text-[38px] md:text-[55px] leading-[46px] md:leading-[63px] mb-2 xl:mb-8">{{ "hero-header" | i18n({}, lang ) | safe }}</h1>
|
||||
<h2 class="secondary-header text-center xl:text-left xl:rtl:text-right font-bold text-[28px] md:text-[38px] leading-[36px] md:leading-[43px] mb-2 xl:mb-8 tracking-[0.01em]">{{ "hero-subheader" | i18n({}, lang ) | safe }}</h2>
|
||||
<p class="landing-page-header-article-paragraph text-black dark:text-white text-center xl:text-justify text-[16px] leading-[24px] mb-[20px] header-description">
|
||||
{{ "hero-p-1" | i18n({}, lang ) | safe }}
|
||||
</p>
|
||||
@@ -87,9 +87,9 @@
|
||||
</div>
|
||||
|
||||
<article class="w-full xl:max-w-[600px] landing-page-header-article px-5">
|
||||
<p class="text-active-blue text-center xl:text-left xl:rtl:text-right font-bold text-[28px] md:text-[35px] leading-[36px] md:leading-[43px] mb-[28px]">
|
||||
<h2 class="text-active-blue text-center xl:text-left xl:rtl:text-right font-bold text-[28px] md:text-[35px] leading-[36px] md:leading-[43px] mb-[28px]">
|
||||
{{ "hero-2-header" | i18n({}, lang ) | safe }}
|
||||
</p>
|
||||
</h2>
|
||||
<p class="text-center text-black dark:text-white xl:text-justify leading-[24px] text-[16px] mb-10 xl:mb-[25px] header-description">
|
||||
{{ "hero-2-header-desc" | i18n({}, lang ) | safe }}
|
||||
</p>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
{% include "navbar.html" %}
|
||||
</section>
|
||||
|
||||
{{ content | safe }}
|
||||
{{ content | applyGlossary | safe }}
|
||||
|
||||
{% include "footer.html" %}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title }}</title>
|
||||
<meta name="description" content="{{ description }}"/>
|
||||
<meta name="Content-Type" content="text/html;charset=utf-8"/>
|
||||
<meta property="og:type" content="website"/>
|
||||
<meta property="og:title" content="{{ title }}"/>
|
||||
<meta property="og:description" content="{{ description }}"/>
|
||||
<meta property="og:image" content="{% cfg 'siteLocation' %}/img/share_simplex.png"/>
|
||||
<meta name="twitter:card" content="summary"/>
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/img/favicon.ico"/>
|
||||
<meta http-equiv="refresh" content="0; url={{ groupLink }}" />
|
||||
</head>
|
||||
<body>
|
||||
<p><a href="{{ groupLink }}">Open RightsCon group link</a></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -67,6 +67,8 @@
|
||||
</a></li>
|
||||
<li><a href="/docs/protocol/simplex-chat.html" class="lg:px-[20px] inline-block"
|
||||
>{{ "chat-protocol" | i18n({}, lang ) | safe }}</a></li>
|
||||
<li><a href="/docs/glossary.html" class="lg:px-[20px] inline-block"
|
||||
>{{ "glossary" | i18n({}, lang ) | safe }}</a></li>
|
||||
|
||||
<hr class=" h-[1px] w-full dark:opacity-[0.1]">
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{# join simplex #}
|
||||
<section id="join-simplex" class="bg-primary-bg-light dark:bg-primary-bg-dark lg:h-[855px] py-[90px] px-5">
|
||||
<div class="container flex flex-col items-center">
|
||||
<p class="text-[38px] leading-[36px] md:leading-[55px] text-grey-black dark:text-white text-center font-bold mb-5"><span class="text-active-blue">{{ "join" | i18n({}, lang ) | safe }}</span> SimpleX</p>
|
||||
<h2 class="text-[38px] leading-[36px] md:leading-[55px] text-grey-black dark:text-white text-center font-bold mb-5"><span class="text-active-blue">{{ "join" | i18n({}, lang ) | safe }}</span> SimpleX</h2>
|
||||
<p class="text-black dark:text-white text-base text-center mb-14">{{ "we-invite-you-to-join-the-conversation" | i18n({}, lang ) | safe }}</p>
|
||||
|
||||
<div class="flex flex-col items-center gap-5 self-stretch mb-12">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<section id="why-simplex" class="bg-primary-bg-light dark:bg-primary-bg-dark py-[90px] overflow-hidden px-0 sm:px-1 xl:h-[888px]">
|
||||
<div class="container scale-100">
|
||||
<p class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-14 px-5 sm:px-4">{{ "why-simplex-is" | i18n({}, lang ) | safe }} <span class="gradient-text">{{ "unique" | i18n({}, lang ) | safe }}</span></p>
|
||||
<h2 class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-14 px-5 sm:px-4">{{ "why-simplex-is" | i18n({}, lang ) | safe }} <span class="gradient-text">{{ "unique" | i18n({}, lang ) | safe }}</span></h2>
|
||||
|
||||
<div class="swiper unique-swiper px-5 sm:px-4 py-2">
|
||||
<div class="swiper-wrapper mb-16">
|
||||
@@ -15,8 +15,8 @@
|
||||
|
||||
<div class="card-content absolute md:static px-4 md:px-0 bottom-[80px] right-1 left-1 h-[180px] md:h-fit pt-5 lg:pt-0 bg-primary-bg-light dark:bg-primary-bg-dark">
|
||||
<div class="content-head">
|
||||
<p class="text-[35px] lg:text-[65px] font-bold tracking-[0.06em] text-active-blue text-center md:text-left md:rtl:text-right">#{{ section.id }}</p>
|
||||
<p class="w-full max-w-[617px] text-[25px] leading-[33px] lg:text-[35px] lg:leading-[45px] text-center md:text-left md:rtl:text-right font-bold text-grey-black dark:text-white">{{ section.title | i18n({}, lang ) | safe }}</p>
|
||||
<h1 class="text-[35px] lg:text-[65px] font-bold tracking-[0.06em] text-active-blue text-center md:text-left md:rtl:text-right">#{{ section.id }}</h1>
|
||||
<h3 class="w-full max-w-[617px] text-[25px] leading-[33px] lg:text-[35px] lg:leading-[45px] text-center md:text-left md:rtl:text-right font-bold text-grey-black dark:text-white">{{ section.title | i18n({}, lang ) | safe }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="content-body py-5 md:py-7">
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<section id="simplex-explained" class="bg-primary-bg-light dark:bg-primary-bg-dark lg:h-[890px] py-[90px] px-5">
|
||||
<div class="container">
|
||||
<p class="text-[35px] leading-[45px] md:leading-[55px] lg:text-[38px] text-center font-bold text-grey-black dark:text-white mb-9">{{ "simplex-explained" | i18n({}, lang ) | safe }}</p>
|
||||
<h2 class="text-[35px] leading-[45px] md:leading-[55px] lg:text-[38px] text-center font-bold text-grey-black dark:text-white mb-9">{{ "simplex-explained" | i18n({}, lang ) | safe }}</h2>
|
||||
|
||||
<!-- Tab links -->
|
||||
<div class="tabs hidden md:flex gap-2 mb-24">
|
||||
|
||||
@@ -35,7 +35,7 @@ active_blog: true
|
||||
|
||||
<section class="py-10 px-5 mt-[66px]" id="blog-list">
|
||||
<div class="container">
|
||||
<p class="text-[38px] text-center font-bold text-active-blue mb-9">Latest news</p>
|
||||
<h1 class="text-[38px] text-center font-bold text-active-blue mb-9">Latest news</h1>
|
||||
|
||||
{% for blog in collections.blogs %}
|
||||
{% if not(blog.data.draft) %}
|
||||
|
||||
+232
-6
@@ -428,13 +428,13 @@ header nav {
|
||||
transform: translateY(40px);
|
||||
}
|
||||
|
||||
.card:not(.no-hover):hover > div:nth-child(1) {
|
||||
.card:not(.no-hover):hover > div:nth-child(1),
|
||||
.card.hovered > div:nth-child(1) {
|
||||
height: 200px;
|
||||
padding: 12px 10px;
|
||||
}
|
||||
.card:not(.no-hover):hover > div:nth-child(2) {
|
||||
.card:not(.no-hover):hover > div:nth-child(2),
|
||||
.card.hovered > div:nth-child(2) {
|
||||
height: 270px;
|
||||
padding: 8px 24px;
|
||||
}
|
||||
.card.card-active > div:nth-child(2) {
|
||||
height: 480px;
|
||||
@@ -443,13 +443,16 @@ header nav {
|
||||
.card:not(.no-hover):hover > div:nth-child(2) > *:nth-child(2),
|
||||
.card:not(.no-hover):hover > div:nth-child(2) > *:nth-child(3),
|
||||
.card.card-active > div:nth-child(2) > *:nth-child(2),
|
||||
.card.card-active > div:nth-child(2) > *:nth-child(3) {
|
||||
.card.card-active > div:nth-child(2) > *:nth-child(3),
|
||||
.card:not(.no-hover).hovered > div:nth-child(2) > *:nth-child(2),
|
||||
.card:not(.no-hover).hovered > div:nth-child(2) > *:nth-child(3) {
|
||||
opacity: 1;
|
||||
max-height: 480px;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
|
||||
.card:not(.no-hover):hover > div:nth-child(2) > *:nth-child(3){
|
||||
.card:not(.no-hover):hover > div:nth-child(2) > *:nth-child(3),
|
||||
.card:not(.no-hover).hovered > div:nth-child(2) > *:nth-child(3) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -710,4 +713,227 @@ p a{
|
||||
.contact-tab.active svg,
|
||||
.contact-tab:hover svg{
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
/* Glossary */
|
||||
.glossary-term{
|
||||
display: inline-block;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dashed;
|
||||
text-underline-offset: 3px;
|
||||
position: relative;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.glossary-term::before{
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
bottom: -5px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.glossary-tooltip {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
position: fixed;
|
||||
width: 320px;
|
||||
max-width: 100%;
|
||||
z-index: 10001;
|
||||
transition: opacity .5s;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.glossary-tooltip .tooltip-content{
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
text-align: left;
|
||||
padding: .8rem 1.2rem;
|
||||
border-radius: 7px;
|
||||
font-size: 14px;
|
||||
line-height: 1.3rem;
|
||||
box-shadow: 0 5px 10px rgba(0,0,0,.08);
|
||||
}
|
||||
|
||||
.dark .glossary-tooltip .tooltip-content{
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.tooltip-title{
|
||||
margin-bottom: 0.5rem;
|
||||
color: #0197FF;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.dark .tooltip-title{
|
||||
color: #70F0F9;
|
||||
}
|
||||
|
||||
.glossary-tooltip .read-more-btn{
|
||||
color: #0053D0;
|
||||
display: block;
|
||||
text-decoration: underline;
|
||||
margin-top: .8rem;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.dark .glossary-tooltip .read-more-btn{
|
||||
color: #70F0F9;
|
||||
}
|
||||
|
||||
.glossary-overlay{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: transparent;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.75rem;
|
||||
z-index: 10005;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.glossary-overlay{
|
||||
padding: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.glossary-overlay::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color:#F3F6F7;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.dark .glossary-overlay::before {
|
||||
content: '';
|
||||
background-color: #0C0B13;
|
||||
}
|
||||
|
||||
.glossary-overlay .overlay-card{
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
opacity: 1;
|
||||
height: 100%;
|
||||
z-index: 10006;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0px 3px 12px rgba(0, 0, 0, 0.2);
|
||||
padding: 2.5rem 1.5rem;
|
||||
overflow: auto;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.dark .glossary-overlay .overlay-card{
|
||||
background-color: #17203D;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.glossary-overlay .overlay-card{
|
||||
padding: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.glossary-overlay .overlay-card{
|
||||
width: fit-content;
|
||||
max-width: 558px;
|
||||
height: fit-content;
|
||||
max-height: 660px;
|
||||
}
|
||||
}
|
||||
|
||||
.glossary-overlay .overlay-card .overlay-title{
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(1 151 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.glossary-overlay .overlay-card .overlay-content{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
color: #3F484B;
|
||||
}
|
||||
|
||||
.dark .glossary-overlay .overlay-card .overlay-content{
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.close-overlay-btn{
|
||||
fill: #3F484B;
|
||||
position: fixed;
|
||||
right: 1.25rem;
|
||||
top: 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dark .close-overlay-btn{
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.glossary-overlay ul {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.glossary-overlay ul,
|
||||
.glossary-overlay ol {
|
||||
list-style-position: inside;
|
||||
overflow: auto;
|
||||
margin: 1rem 0;
|
||||
/* padding-left: 1rem; */
|
||||
}
|
||||
|
||||
.glossary-overlay ul li,
|
||||
.glossary-overlay ol li {
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
-webkit-margin-start: 1.1rem;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.dark .glossary-overlay ul li,
|
||||
.dark .glossary-overlay ol li {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.glossary-overlay ul li::marker,
|
||||
.glossary-overlay ol li::marker {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.dark .glossary-overlay ul li::marker,
|
||||
.dark .glossary-overlay ol li::marker {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.glossary-overlay ul li a,
|
||||
.glossary-overlay ol li a {
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.glossary-overlay ul li {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
.glossary-overlay ol li {
|
||||
list-style: decimal;
|
||||
}
|
||||
+10
-10
@@ -11,7 +11,7 @@ active_home: true
|
||||
|
||||
<section id="why-privacy" class="bg-secondary-bg-light dark:bg-secondary-bg-dark py-[90px] px-5 lg:h-[888px]">
|
||||
<div class="container">
|
||||
<p class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-4 md:mb-8">{{ "privacy-matters-section-header" | i18n({}, lang ) | safe }}</p>
|
||||
<h2 class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-4 md:mb-8">{{ "privacy-matters-section-header" | i18n({}, lang ) | safe }}</h2>
|
||||
<p class="text-center text-[18px] md:text-[20px] font-medium mb-7 md:mb-16 lg:mb-20 text-black dark:text-white">{{ "privacy-matters-section-subheader" | i18n({}, lang ) | safe }}</p>
|
||||
<div class="flex flex-col lg:flex-row gap-[20px] mb-[62px] lg:mb-[90px]">
|
||||
|
||||
@@ -41,7 +41,7 @@ active_home: true
|
||||
{# Features #}
|
||||
<section id="features" class="bg-secondary-bg-light dark:bg-secondary-bg-dark py-[95px] px-5 lg:h-[888px]">
|
||||
<div class="container">
|
||||
<p class="text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold gradient-text mb-20">{{ "features" | i18n({}, lang ) | safe }}</p>
|
||||
<h2 class="text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold gradient-text mb-20">{{ "features" | i18n({}, lang ) | safe }}</h2>
|
||||
|
||||
<div class="mb-[50px] grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-x-10 gap-y-32">
|
||||
{% for feature in features.sections %}
|
||||
@@ -60,7 +60,7 @@ active_home: true
|
||||
{# what makes simplex private #}
|
||||
<section id="privacy" class="bg-primary-bg-light dark:bg-primary-bg-dark py-[90px] overflow-hidden px-5 lg:h-[888px]">
|
||||
<div class="container scale-100">
|
||||
<p class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-20">{{ "simplex-private-section-header" | i18n({}, lang ) | safe }}</p>
|
||||
<h2 class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-20">{{ "simplex-private-section-header" | i18n({}, lang ) | safe }}</h2>
|
||||
|
||||
<div class="swiper private-swiper overflow-hidden px-4 py-2">
|
||||
<div class="swiper-wrapper mb-16">
|
||||
@@ -71,8 +71,8 @@ active_home: true
|
||||
<img class="w-full max-w-[223px] h-full max-h-[226px] dark:hidden" src="{{ section.imgLight }}" alt=""/>
|
||||
<img class="w-full max-w-[223px] h-full max-h-[226px] hidden dark:block" src="{{ section.imgDark }}" alt=""/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-between h-[138px] absolute bottom-0 py-6 px-6 bg-card-desc-bg-light dark:bg-card-desc-bg-dark rounded-b-[20px]">
|
||||
<p class="text-grey-black dark:text-white text-[18px] my-4 font-bold leading-[26px] tracking-[0.01em] text-center">{{ section.title | i18n({}, lang ) | safe }}</p>
|
||||
<div class="card-content flex flex-col items-center justify-between h-[138px] absolute bottom-0 py-6 px-6 bg-card-desc-bg-light dark:bg-card-desc-bg-dark rounded-b-[20px]">
|
||||
<h3 class="text-grey-black dark:text-white text-[18px] my-4 font-bold leading-[26px] tracking-[0.01em] text-center">{{ section.title | i18n({}, lang ) | safe }}</h3>
|
||||
<div class="flex-1 py-3 flex flex-col gap-3">
|
||||
{% for point in section.points %}
|
||||
<p class="text-grey-black dark:text-white text-[14px] text-center">{{ point | i18n({}, lang ) | safe }}</p>
|
||||
@@ -102,7 +102,7 @@ active_home: true
|
||||
{# Network #}
|
||||
<section id="network" class="bg-secondary-bg-light dark:bg-secondary-bg-dark lg:h-[642px] py-[95px] px-5">
|
||||
<div class="container">
|
||||
<p class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-5">{{ "simplex-network-section-header" | i18n({}, lang ) | safe }}</p>
|
||||
<h2 class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-5">{{ "simplex-network-section-header" | i18n({}, lang ) | safe }}</h2>
|
||||
<p class="text-black dark:text-white text-[16px] font-normal text-center mb-16">{{ "simplex-network-section-desc" | i18n({}, lang ) | safe }}</p>
|
||||
|
||||
<div class="flex flex-col lg:flex-row justify-between gap-12 md:gap-14 lg:gap-16">
|
||||
@@ -112,7 +112,7 @@ active_home: true
|
||||
<img src="/img/new/network-1-dark.svg" alt="" class="hidden dark:block"/>
|
||||
</div>
|
||||
<div class="md:flex-[2] flex flex-col items-center justify-center">
|
||||
<p class="text-active-blue text-xl font-bold text-center md:text-left lg:text-center self-stretch">{{ "simplex-network-1-header" | i18n({}, lang ) | safe }}</p>
|
||||
<h3 class="text-active-blue text-xl font-bold text-center md:text-left lg:text-center self-stretch">{{ "simplex-network-1-header" | i18n({}, lang ) | safe }}</h3>
|
||||
<p class="text-black dark:text-white text-base font-normal text-center md:text-left lg:text-center">
|
||||
{{ "simplex-network-1-desc" | i18n({}, lang ) | safe }} <a href="javascript:void(0)" data-show-overlay="{{ simplex_network_overlay.sections[0].overlayContent.overlayId }}" class="open-overlay-btn">{{ "simplex-network-1-overlay-linktext" | i18n({}, lang ) | safe }}</a>.
|
||||
{{ overlay(simplex_network_overlay.sections[0],lang) }}
|
||||
@@ -128,7 +128,7 @@ active_home: true
|
||||
<img src="/img/new/network-2-dark.svg" alt="" class="hidden dark:block"/>
|
||||
</div>
|
||||
<div class="md:flex-[2] flex flex-col items-center justify-center">
|
||||
<p class="text-active-blue text-xl font-bold text-center md:text-left lg:text-center self-stretch">{{ "simplex-network-2-header" | i18n({}, lang ) | safe }}</p>
|
||||
<h3 class="text-active-blue text-xl font-bold text-center md:text-left lg:text-center self-stretch">{{ "simplex-network-2-header" | i18n({}, lang ) | safe }}</h3>
|
||||
<p class="text-black dark:text-white text-base font-normal text-center md:text-left lg:text-center">
|
||||
{{ "simplex-network-2-desc" | i18n({}, lang ) | safe }}
|
||||
</p>
|
||||
@@ -143,7 +143,7 @@ active_home: true
|
||||
<img src="/img/new/network-3-dark.svg" alt="" class="hidden dark:block"/>
|
||||
</div>
|
||||
<div class="md:flex-[2] flex flex-col items-center justify-center">
|
||||
<p class="text-active-blue text-xl font-bold text-center md:text-left lg:text-center self-stretch">{{ "simplex-network-3-header" | i18n({}, lang ) | safe }}</p>
|
||||
<h3 class="text-active-blue text-xl font-bold text-center md:text-left lg:text-center self-stretch">{{ "simplex-network-3-header" | i18n({}, lang ) | safe }}</h3>
|
||||
<p class="text-black dark:text-white text-base font-normal text-center md:text-left lg:text-center">
|
||||
{{ "simplex-network-3-desc" | i18n({}, lang ) | safe }}
|
||||
</p>
|
||||
@@ -163,7 +163,7 @@ active_home: true
|
||||
{# Comparison #}
|
||||
<section id="comparison" class="bg-secondary-bg-light dark:bg-secondary-bg-dark lg:h-[950px] py-[90px] px-5">
|
||||
<div class="text-grey-black dark:text-white container flex flex-col">
|
||||
<p class="text-[35px] leading-[43px] md:leading-[55px] lg:leading-[36px] text-center font-bold mb-12 lg:mb-[90px]">{{ "comparison-section-header" | i18n({}, lang ) | safe }}</p>
|
||||
<h2 class="text-[35px] leading-[43px] md:leading-[55px] lg:leading-[36px] text-center font-bold mb-12 lg:mb-[90px]">{{ "comparison-section-header" | i18n({}, lang ) | safe }}</h2>
|
||||
|
||||
<div class="w-full overflow-auto">
|
||||
<table class="w-full border-separate border-spacing-x-5 border-spacing-y-2 mb-14">
|
||||
|
||||
+139
-2
@@ -65,6 +65,19 @@ const privateSwiper = new Swiper('.private-swiper', {
|
||||
allowTouchMove: true,
|
||||
}
|
||||
},
|
||||
on: {
|
||||
slideChange: function () {
|
||||
const privateSwiperGlossaryTerms = document.querySelectorAll('.private-swiper .glossary-term');
|
||||
privateSwiperGlossaryTerms.forEach(function (glossaryTerm) {
|
||||
var tooltipId = glossaryTerm.getAttribute('data-glossary');
|
||||
var tooltip = document.getElementById(tooltipId);
|
||||
tooltip.style.visibility = 'hidden';
|
||||
tooltip.style.opacity = '0';
|
||||
const privateSwiper = glossaryTerm.closest('.private-swiper')
|
||||
if (privateSwiper) glossaryTerm.closest('.card').classList.remove('hovered');
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const simplexExplainedSwiper = new Swiper(".simplex-explained-swiper", {
|
||||
@@ -86,6 +99,17 @@ const simplexExplainedSwiper = new Swiper(".simplex-explained-swiper", {
|
||||
pagination: {
|
||||
el: ".simplex-explained-swiper-pagination",
|
||||
clickable: true
|
||||
},
|
||||
on: {
|
||||
slideChange: function () {
|
||||
const explainedSwiperGlossaryTerms = document.querySelectorAll('.simplex-explained-swiper .glossary-term');
|
||||
explainedSwiperGlossaryTerms.forEach(function (glossaryTerm) {
|
||||
var tooltipId = glossaryTerm.getAttribute('data-glossary');
|
||||
var tooltip = document.getElementById(tooltipId);
|
||||
tooltip.style.visibility = 'hidden';
|
||||
tooltip.style.opacity = '0';
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -163,6 +187,13 @@ function openOverlay() {
|
||||
const scrollToEl = document.getElementById(scrollTo)
|
||||
if (scrollToEl) scrollToEl.scrollIntoView(true)
|
||||
}
|
||||
|
||||
const currentOpenedGlossaryOverlay = document.querySelector('.glossary-overlay.flex')
|
||||
if (currentOpenedGlossaryOverlay) {
|
||||
currentOpenedGlossaryOverlay.classList.remove('flex')
|
||||
currentOpenedGlossaryOverlay.classList.add('hidden')
|
||||
}
|
||||
|
||||
el.classList.remove('hidden')
|
||||
el.classList.add('flex')
|
||||
document.body.classList.add('lock-scroll')
|
||||
@@ -170,5 +201,111 @@ function openOverlay() {
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('load', openOverlay);
|
||||
window.addEventListener('hashchange', openOverlay);
|
||||
function updatePointerEventsInPrivateSwiperCards() {
|
||||
var privateSwiperCards = document.querySelectorAll('.private-swiper .card')
|
||||
|
||||
privateSwiperCards.forEach(function (card) {
|
||||
var cardContent = card.querySelector('.card-content')
|
||||
|
||||
function updatePointerEvents() {
|
||||
var cardContentGlossaryTerms = cardContent.querySelectorAll('.glossary-term')
|
||||
cardContentGlossaryTerms.forEach(function (glossaryTerm) {
|
||||
if (cardContent.offsetHeight >= 270) {
|
||||
glossaryTerm.style.pointerEvents = 'all'
|
||||
} else {
|
||||
glossaryTerm.style.pointerEvents = 'none'
|
||||
}
|
||||
})
|
||||
}
|
||||
updatePointerEvents()
|
||||
|
||||
cardContent.addEventListener('click', updatePointerEvents)
|
||||
cardContent.addEventListener('mousemove', updatePointerEvents)
|
||||
})
|
||||
}
|
||||
|
||||
function updateTooltipPosition(glossaryTerm, tooltip) {
|
||||
var glossaryTermOffset = glossaryTerm.getBoundingClientRect()
|
||||
var tooltipOffset = tooltip.getBoundingClientRect()
|
||||
|
||||
if (glossaryTermOffset.top >= tooltipOffset.height) {
|
||||
tooltip.style.top = glossaryTermOffset.top - tooltipOffset.height + 'px'
|
||||
} else {
|
||||
tooltip.style.top = glossaryTermOffset.bottom + 'px'
|
||||
}
|
||||
|
||||
var leftPosition = glossaryTermOffset.left + glossaryTerm.offsetWidth / 2 - tooltip.offsetWidth / 2
|
||||
if (leftPosition < 0) {
|
||||
tooltip.style.left = '0px'
|
||||
} else if (leftPosition + tooltip.offsetWidth > window.innerWidth) {
|
||||
tooltip.style.left = window.innerWidth - tooltip.offsetWidth + 'px'
|
||||
} else {
|
||||
tooltip.style.left = leftPosition + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
function setupTooltip(glossaryTerm) {
|
||||
var tooltipId = glossaryTerm.getAttribute('data-glossary')
|
||||
var tooltip = document.getElementById(tooltipId)
|
||||
|
||||
function showTooltip() {
|
||||
tooltip.style.visibility = 'visible'
|
||||
tooltip.style.opacity = '1'
|
||||
updateTooltipPosition(glossaryTerm, tooltip)
|
||||
const privateSwiper = glossaryTerm.closest('.private-swiper')
|
||||
if (privateSwiper) glossaryTerm.closest('.card').classList.add('hovered')
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
tooltip.style.visibility = 'hidden'
|
||||
tooltip.style.opacity = '0'
|
||||
const privateSwiper = glossaryTerm.closest('.private-swiper')
|
||||
if (privateSwiper) glossaryTerm.closest('.card').classList.remove('hovered')
|
||||
if (!glossaryTerm.matches(':hover') && !tooltip.matches(':hover')) {
|
||||
glossaryTerm.classList.remove('active-term')
|
||||
tooltip.removeEventListener('mouseover', showTooltip)
|
||||
tooltip.removeEventListener('mouseout', hideTooltip)
|
||||
}
|
||||
}
|
||||
|
||||
let click = 0
|
||||
glossaryTerm.addEventListener('mouseover', () => {
|
||||
glossaryTerm.classList.add('active-term')
|
||||
showTooltip()
|
||||
tooltip.addEventListener('mouseover', showTooltip)
|
||||
tooltip.addEventListener('mouseout', hideTooltip)
|
||||
})
|
||||
glossaryTerm.addEventListener('mouseout', function (event) {
|
||||
click = 0
|
||||
hideTooltip()
|
||||
})
|
||||
glossaryTerm.addEventListener('click', function (event) {
|
||||
event.stopPropagation()
|
||||
if (click == 1) {
|
||||
hideTooltip()
|
||||
click = 0
|
||||
}
|
||||
else {
|
||||
showTooltip()
|
||||
click = 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
openOverlay()
|
||||
updatePointerEventsInPrivateSwiperCards()
|
||||
|
||||
window.addEventListener('scroll', function () {
|
||||
let activeTerm = document.querySelector('.active-term')
|
||||
if (activeTerm) {
|
||||
var tooltipId = activeTerm.getAttribute('data-glossary')
|
||||
var tooltip = document.getElementById(tooltipId)
|
||||
updateTooltipPosition(activeTerm, tooltip)
|
||||
}
|
||||
})
|
||||
|
||||
const glossaryTerms = document.querySelectorAll('.glossary-term')
|
||||
glossaryTerms.forEach(setupTooltip)
|
||||
})
|
||||
window.addEventListener('hashchange', openOverlay)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
layout: layouts/rightscon.html
|
||||
title: "SimpleX Chat - RightsCon group"
|
||||
description: "Join the group of attendees of RightsCon 2023"
|
||||
groupLink: "https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FyHKMxr06RLUiKent0IREl1rwUtsc1MKs%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAwxplfiUKydkqy7Rbl-YQCWUSnrV_ADSd5fWvH17BvEs%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22B3dC9QiKk4AEpGWaLUuPxw%3D%3D%22%7D"
|
||||
templateEngineOverride: njk
|
||||
---
|
||||
Reference in New Issue
Block a user