Compare commits

...

28 Commits

Author SHA1 Message Date
spaced4ndy 3267b4d6ca 5.1.3: Android 127, iOS 150 2023-06-12 16:13:46 +04:00
spaced4ndy 9b302b856a ios: update binaries 2023-06-12 14:46:26 +04:00
spaced4ndy 4e696aed82 core: 5.1.3.0 2023-06-12 13:47:13 +04:00
spaced4ndy 425c7b947f core: optimize group deletion (delays deletion of unused contacts) (#2560)
* core: optimize group deletion (wip)

* delay deletion of unused contacts

* clean up, fix test

* rename field

* remove from type, more checks, remove ctx

* remove space

* rename functions

* rename

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-06-12 13:45:39 +04:00
M Sarmad Qadeer d4f9429fc1 website: fix sidebar duplication issue (#2576) 2023-06-11 10:39:59 +01:00
Evgeny Poberezkin 161b43e85d ci: update node verion for website build 2023-06-11 08:40:47 +01:00
M Sarmad Qadeer d585e8f5a7 website: glossary extended (#2574)
* web: quick fixes in glossary.md

* website: update header tags

* website: add glossary feature

* website: add & style the tooltips & glossary terms

* website: add overlay for glossary definition

* website: add list styling & update links

* website: fix tooltip alignment against multiple terms
fix the issue of aligning tooltip if page has multiple terms against it

* website: close already opened glossary overlay
close the already opened glossary overlay before opening another

* website: add popups for terms inside popups

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-06-11 08:31:41 +01:00
M Sarmad Qadeer 060e7cdf52 website: add glossary in reference dropdown (#2573) 2023-06-11 07:58:00 +01:00
M Sarmad Qadeer 6fa002948e website: fix logo color issue in dark mode in footer (#2572) 2023-06-11 07:54:31 +01:00
M Sarmad Qadeer bbd4e6c8ba website: glossary feature (#2529)
* web: quick fixes in glossary.md

* website: update header tags

* website: add glossary feature

* website: add & style the tooltips & glossary terms

* website: add overlay for glossary definition

* website: add list styling & update links

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-06-11 07:52:55 +01:00
spaced4ndy 92cf945e10 core: update default mobile servers (#2548) 2023-06-11 07:48:03 +01:00
spaced4ndy cc0f55c245 core, mobile: track contact connection network status when new contact joins group (#2566) 2023-06-09 16:43:53 +04:00
spaced4ndy 22f27c4255 mobile: allow to leave group in accepted status (#2564) 2023-06-09 14:40:17 +04:00
Stanislav Dmitrenko 14a888bf43 ios: show video in call from simulator (#2562) 2023-06-08 16:09:14 +01:00
Stanislav Dmitrenko f6fddc9436 ios: WebRTC decryption between iOS-iOS works (#2561) 2023-06-08 15:08:35 +01:00
spaced4ndy f581e91f19 core, mobile: correctly check whether date is recent (#2559) 2023-06-08 11:23:04 +04:00
spaced4ndy fb72dfcdee core: calculate local item ts in view instead of having it in type (#2551) 2023-06-08 11:07:21 +04:00
Evgeny Poberezkin 925813b14c website: redirect page 2023-06-07 21:04:19 +01:00
sh abd410fe62 build-android: refactor script (allow to parameterize script with architecture so builds can be done separately) (#2532)
* build-android: refactor script

* rearrange folder varibale

* fix gradle
2023-06-07 10:27:52 +04:00
spaced4ndy 875282e9ec core: fix parsing multiple servers passed as cli argument (#2549) 2023-06-06 14:17:14 +04:00
Evgeny Poberezkin 6afda28367 cli: change active chat after tail command (#2547) 2023-06-05 10:38:25 +04:00
Aminda Suomalainen 0721b24250 docs/SERVER: use sudo, specify protocol & add Fedora (#2542)
* docs/SERVER.md: specify protocol for ufw, use sudo & mention Fedora

* docs/lang/*/SERVER.md: update firewall instructions

* docs/SERVER (& lang): also reload firewalld

The manual page notes that --permanent is not effective immediately

* docs/{WEBRTC,XFTP-SERVER}.md: firewalld instructions

* docs/lang/*/WEBRTC.md: update firewall instructions from English

* Update docs/XFTP-SERVER.md

Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>

* Update docs/SERVER.md

Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>

* Update docs/lang/cs/SERVER.md

Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>

* Update docs/lang/fr/SERVER.md

Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>

* Update docs/lang/cs/WEBRTC.md

Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>

* Update docs/lang/fr/WEBRTC.md

Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>

* Update docs/WEBRTC.md

Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>

---------

Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>
2023-06-03 08:43:37 -07:00
spaced4ndy 10b6bce8a2 docs: connection switch improvements rfc (#2537) 2023-06-01 21:04:51 +04:00
Evgeny Poberezkin 0101444c5d cli: increase image size to match mobile auto-accept size limit, allow png and gif (#2535) 2023-05-30 11:18:27 +01:00
spaced4ndy 128883b8a3 core: improve queries performance; delay first chat item expiration cycle on start (#2521) 2023-05-29 15:18:22 +04:00
spaced4ndy cc75b75d4e core: remove timing events (#2530) 2023-05-29 11:19:03 +04:00
Evgeny Poberezkin dea6cd81c7 android: fix nl translation 2023-05-28 20:52:03 +01:00
sh 2f53ab08b5 github: update templates (#2527) 2023-05-28 20:51:31 +01:00
60 changed files with 1540 additions and 418 deletions
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
name: Question
description: Ask your question
title: "[Q]: "
labels: ["type:question", "type:triage"]
labels: ["question", "triage"]
body:
- type: markdown
attributes:
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
node-version: [12.x]
node-version: [16.x]
steps:
- uses: actions/checkout@v2
+2 -2
View File
@@ -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>
+4 -1
View File
@@ -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: {
+28 -28
View File
@@ -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 = "";
+2 -2
View File
@@ -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)")
+16 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+7 -4
View File
@@ -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
View File
@@ -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í
+7 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
+61 -36
View File
@@ -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
View File
@@ -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
View File
@@ -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}
+10 -3
View File
@@ -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}
+4 -7
View File
@@ -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;
|]
+13 -1
View File
@@ -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
);
+12 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+15 -4
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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
+86
View File
@@ -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
View File
@@ -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) {
+2 -1
View File
@@ -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"
}
+4 -1
View File
@@ -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"
}
}
+110
View File
@@ -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"
}
]
+1 -1
View File
@@ -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>
+6 -6
View File
@@ -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>
+1 -1
View File
@@ -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">
+4 -4
View File
@@ -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>
+1 -1
View File
@@ -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>
+2
View File
@@ -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">
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+7
View File
@@ -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
---