Merge branch 'master' into master-ios

This commit is contained in:
spaced4ndy
2023-06-09 17:31:57 +04:00
29 changed files with 607 additions and 226 deletions
@@ -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)
+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: {
+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 {
+4 -1
View File
@@ -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:
+4 -1
View File
@@ -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í
+4 -1
View File
@@ -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.
+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() {
+1
View File
@@ -374,6 +374,7 @@ test-suite simplex-chat-test
MobileTests
ProtocolTests
SchemaDump
ViewTests
WebRTCTests
Paths_simplex_chat
hs-source-dirs:
+19 -17
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
@@ -69,7 +68,7 @@ import Simplex.Messaging.Agent.Client (AgentStatsKey (..), temporaryAgentError)
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
import Simplex.Messaging.Agent.Lock
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withTransactionCtx)
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration)
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
import Simplex.Messaging.Client (defaultNetworkConfig)
import qualified Simplex.Messaging.Crypto as C
@@ -137,7 +136,10 @@ _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
@@ -352,9 +354,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, [])
@@ -1257,7 +1259,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
@@ -1471,6 +1473,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
@@ -1502,7 +1505,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
@@ -2783,7 +2786,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 +2935,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 +3277,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
@@ -4416,10 +4419,9 @@ 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
@@ -4985,7 +4987,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}
+9 -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)
@@ -143,6 +143,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 +413,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 +491,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
+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
+27 -35
View File
@@ -320,7 +320,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
@@ -3787,9 +3786,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|
@@ -3843,18 +3841,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|
@@ -3915,10 +3912,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
@@ -4026,9 +4023,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|
@@ -4056,9 +4052,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|
@@ -4087,9 +4082,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|
@@ -4523,9 +4517,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
@@ -4684,9 +4677,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
@@ -5135,8 +5127,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
@@ -5163,14 +5155,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
@@ -5185,8 +5177,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_
@@ -5219,14 +5211,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} =
+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} =
+34 -16
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 =
+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
@@ -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>
+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
---