mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Merge branch 'master' into master-ios
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -456,7 +456,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case groupInvitation(user: User, groupInfo: GroupInfo) // unused
|
||||
case userJoinedGroup(user: User, groupInfo: GroupInfo)
|
||||
case joinedGroupMember(user: User, groupInfo: GroupInfo, member: GroupMember)
|
||||
case connectedToGroupMember(user: User, groupInfo: GroupInfo, member: GroupMember)
|
||||
case connectedToGroupMember(user: User, groupInfo: GroupInfo, member: GroupMember, memberContact: Contact?)
|
||||
case groupRemoved(user: User, groupInfo: GroupInfo) // unused
|
||||
case groupUpdated(user: User, toGroup: GroupInfo)
|
||||
case groupLinkCreated(user: User, groupInfo: GroupInfo, connReqContact: String, memberRole: GroupMemberRole)
|
||||
@@ -692,7 +692,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .groupInvitation(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .userJoinedGroup(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .joinedGroupMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .connectedToGroupMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .connectedToGroupMember(u, groupInfo, member, memberContact): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nmemberContact: \(String(describing: memberContact))")
|
||||
case let .groupRemoved(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
|
||||
case let .groupLinkCreated(u, groupInfo, connReqContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)\nmemberRole: \(memberRole)")
|
||||
|
||||
@@ -2283,10 +2283,22 @@ let msgTimeFormat = Date.FormatStyle.dateTime.hour().minute()
|
||||
let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits)
|
||||
|
||||
public func formatTimestampText(_ date: Date) -> Text {
|
||||
let now = Calendar.current.dateComponents([.day, .hour], from: .now)
|
||||
let dc = Calendar.current.dateComponents([.day, .hour], from: date)
|
||||
let recent = now.day == dc.day || ((now.day ?? 0) - (dc.day ?? 0) == 1 && (dc.hour ?? 0) >= 18 && (now.hour ?? 0) < 12)
|
||||
return Text(date, format: recent ? msgTimeFormat : msgDateFormat)
|
||||
return Text(date, format: recent(date) ? msgTimeFormat : msgDateFormat)
|
||||
}
|
||||
|
||||
private func recent(_ date: Date) -> Bool {
|
||||
let now = Date()
|
||||
let calendar = Calendar.current
|
||||
|
||||
guard let previousDay = calendar.date(byAdding: DateComponents(day: -1), to: now),
|
||||
let previousDay18 = calendar.date(bySettingHour: 18, minute: 0, second: 0, of: previousDay),
|
||||
let currentDay00 = calendar.date(bySettingHour: 0, minute: 0, second: 0, of: now),
|
||||
let currentDay12 = calendar.date(bySettingHour: 12, minute: 0, second: 0, of: now) else {
|
||||
return false
|
||||
}
|
||||
|
||||
let isSameDay = calendar.isDate(date, inSameDayAs: now)
|
||||
return isSameDay || (now < currentDay12 && date >= previousDay18 && date < currentDay00)
|
||||
}
|
||||
|
||||
public enum CIStatus: Decodable {
|
||||
|
||||
+4
-1
@@ -47,7 +47,10 @@ Manual installation requires some preliminary actions:
|
||||
|
||||
```sh
|
||||
# For Ubuntu
|
||||
ufw allow 5223
|
||||
sudo ufw allow 5223/tcp
|
||||
# For Fedora
|
||||
sudo firewall-cmd --permanent --add-port=5223/tcp && \
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
4. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/smp-server.service` file with the following content:
|
||||
|
||||
+15
-5
@@ -74,11 +74,21 @@ systemctl enable coturn && systemctl start coturn
|
||||
- **49152:65535** – port range that Coturn will use by default for TURN relay.
|
||||
|
||||
```sh
|
||||
ufw allow 3478 && \
|
||||
ufw allow 443 && \
|
||||
ufw allow 5349 && \
|
||||
ufw allow 49152:65535/tcp && \
|
||||
ufw allow 49152:65535/udp
|
||||
# For Ubuntu
|
||||
sudo ufw allow 3478 && \
|
||||
sudo ufw allow 443 && \
|
||||
sudo ufw allow 5349 && \
|
||||
sudo ufw allow 49152:65535/tcp && \
|
||||
sudo ufw allow 49152:65535/udp
|
||||
|
||||
# For Fedora
|
||||
sudo firewall-cmd --permanent --add-port=443/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=443/udp && \
|
||||
sudo firewall-cmd --permanent --add-port=5349/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=5349/udp && \
|
||||
sudo firewall-cmd --permanent --add-port=49152:65535/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=49152:65535/udp && \
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
## Configure mobile apps
|
||||
|
||||
+4
-1
@@ -40,7 +40,10 @@ XFTP is a new file transfer protocol focussed on meta-data protection - it is ba
|
||||
|
||||
```sh
|
||||
# For Ubuntu
|
||||
sudo ufw allow 443
|
||||
sudo ufw allow 443/tcp
|
||||
# For Fedora
|
||||
sudo firewall-cmd --permanent --add-port=443/tcp && \
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
5. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/xftp-server.service` file with the following content:
|
||||
|
||||
@@ -46,7 +46,10 @@ Ruční instalace vyžaduje několik předběžných úkonů:
|
||||
|
||||
```sh
|
||||
# Pro Ubuntu
|
||||
ufw allow 5233
|
||||
sudo ufw allow 5233/tcp
|
||||
# Pro Fedora
|
||||
sudo firewall-cmd --permanent --add-port=5223/tcp && \
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
4. **Volitelné** - Pokud používáte distribuci s `systemd`, vytvořte soubor `/etc/systemd/system/smp-server.service` s následujícím obsahem:
|
||||
|
||||
+15
-5
@@ -73,11 +73,21 @@ systemctl enable coturn && systemctl start coturn
|
||||
- **49152:65535** - rozsah portů, který bude společnost Coturn ve výchozím nastavení používat pro přenos TURN.
|
||||
|
||||
```sh
|
||||
ufw allow 3478 && \
|
||||
ufw allow 443 && \
|
||||
ufw allow 5349 && \
|
||||
ufw allow 49152:65535/tcp && \
|
||||
ufw allow 49152:65535/udp
|
||||
# Pro Ubuntu
|
||||
sudo ufw allow 3478 && \
|
||||
sudo ufw allow 443 && \
|
||||
sudo ufw allow 5349 && \
|
||||
sudo ufw allow 49152:65535/tcp && \
|
||||
sudo ufw allow 49152:65535/udp
|
||||
|
||||
# Pro Fedora
|
||||
sudo firewall-cmd --permanent --add-port=443/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=443/udp && \
|
||||
sudo firewall-cmd --permanent --add-port=5349/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=5349/udp && \
|
||||
sudo firewall-cmd --permanent --add-port=49152:65535/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=49152:65535/udp && \
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
## Konfigurace mobilních aplikací
|
||||
|
||||
@@ -46,7 +46,10 @@ L'installation manuelle nécessite quelques actions préalables :
|
||||
|
||||
```sh
|
||||
# Pour Ubuntu
|
||||
ufw allow 5223
|
||||
sudo ufw allow 5223/tcp
|
||||
# Pour Fedora
|
||||
sudo firewall-cmd --permanent --add-port=5223/tcp && \
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
4. **Optionnel** - Si vous utilisez une distribution avec `systemd`, créez le fichier `/etc/systemd/system/smp-server.service` avec le contenu suivant :
|
||||
|
||||
+15
-5
@@ -73,11 +73,21 @@ systemctl enable coturn && systemctl start coturn
|
||||
- **49152:65535** – plage de ports que Coturn utilisera par défaut pour le relais TURN.
|
||||
|
||||
```sh
|
||||
ufw allow 3478 && \
|
||||
ufw allow 443 && \
|
||||
ufw allow 5349 && \
|
||||
ufw allow 49152:65535/tcp && \
|
||||
ufw allow 49152:65535/udp
|
||||
# Pour Ubuntu
|
||||
sudo ufw allow 3478 && \
|
||||
sudo ufw allow 443 && \
|
||||
sudo ufw allow 5349 && \
|
||||
sudo ufw allow 49152:65535/tcp && \
|
||||
sudo ufw allow 49152:65535/udp
|
||||
|
||||
# Pour Fedora
|
||||
sudo firewall-cmd --permanent --add-port=443/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=443/udp && \
|
||||
sudo firewall-cmd --permanent --add-port=5349/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=5349/udp && \
|
||||
sudo firewall-cmd --permanent --add-port=49152:65535/tcp && \
|
||||
sudo firewall-cmd --permanent --add-port=49152:65535/udp && \
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
## Configurer l'app mobile
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# Connection switch improvements
|
||||
|
||||
## Problems
|
||||
|
||||
- State of switch is unknown to chat, making it unclear if switch is progressing or stuck.
|
||||
|
||||
- Multiple connection switches can be triggered before the first one completes, leaving unused queues.
|
||||
|
||||
## Solution
|
||||
|
||||
Propagate switch state to UI via extended `ConnectionStats`. UI will display switch status if switch is in progress, the button to initiate switch will be disabled depending on switch state.
|
||||
|
||||
### Agent backend
|
||||
|
||||
Add following fields to `connections` table:
|
||||
|
||||
```sql
|
||||
ALTER TABLE connections ADD COLUMN rcv_switch_status TEXT;
|
||||
ALTER TABLE connections ADD COLUMN snd_switch_status TEXT;
|
||||
```
|
||||
|
||||
We can use either existing `SwitchPhase` type as status, or new more detailed types separate for rcv and snd switch.
|
||||
|
||||
```haskell
|
||||
data RcvSwitchStatus
|
||||
= RSSQueuingSwch -- set in beginning of switchConnectionAsync' before queueing SWCH command
|
||||
| RSSSwchStarted -- set in beginning of switchConnection'
|
||||
| RSSQueuingQADD -- set in switchConnection' before queuing QADD
|
||||
| RSSSentQADD -- set in runSmpQueueMsgDelivery after receiving Right in response to sending QADD
|
||||
| RSSReceivedQKEY -- set on receiving QKEY, in beginning of qKeyMsg
|
||||
| RSSQueueingSecure -- set before queueing ICQSecure in qKeyMsg
|
||||
| RSSSecureStarted -- set in beginning of ICQSecure processing in runCommandProcessing
|
||||
| RSSQueueingQUSE -- set in ICQSecure processing before queueing QUSE
|
||||
| RSSMessageReceived -- set after receiving first message in the new queue (processSMPTransmission, setRcvQueuePrimary)
|
||||
| RSSQueueingDelete -- set before queuing ICQDelete in processSMPTransmission
|
||||
| RSSDeleteStarted -- set in beginning of ICQDelete processing in runCommandProcessing
|
||||
|
||||
-- after ICQDelete processing rcv_switch_status is set back to NULL, on internal errors as well
|
||||
|
||||
canStopRcvSwitch :: RcvSwitchStatus -> Bool
|
||||
canStopRcvSwitch = \case
|
||||
RSSSentQADD -> True
|
||||
_ -> False
|
||||
```
|
||||
|
||||
I don't know use for most of the statuses yet, except for debugging, and more granular control over converting switch state into UI representation. At least it seems necessary to record the very beginning of switch (async command, sync command), points of sending QADD and receiving QKEY, and to reset after ICQDelete. `SwitchPhase` type seems to be insufficient.
|
||||
|
||||
When it is in progress, repeatedly switching connection in UI is only allowed in RSSSentQADD status - to avoid race conditions with internal commands processing, and sender processing.
|
||||
|
||||
Logic of repeat switch of connection could be split into stopSwitchConnection and existing start switch command on client level (basically chat automating two buttons for user).
|
||||
|
||||
```haskell
|
||||
stopSwitchConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m ConnectionStats
|
||||
stopSwitchConnection c = do
|
||||
-- in transaction:
|
||||
-- if rcv_switch_status is RSSSentQADD:
|
||||
-- - deleteConnRcvQueue
|
||||
-- - reset rcv_switch_status to NULL
|
||||
-- else: throw error
|
||||
-- deleteQueue on server
|
||||
```
|
||||
|
||||
Repeat switch would send repeat QADD to sender, sender should delete snd queue that was previously set to replace existing queue (check in qAddMsg).
|
||||
|
||||
Snd switch statuses:
|
||||
|
||||
```haskell
|
||||
data SndSwitchStatus
|
||||
= SSSReceivedQADD -- set on receiving QADD, in beginning of qAddMsg
|
||||
| SSSQueueingQKEY -- set in qAddMsg before queuing QKEY
|
||||
| SSSReceivedQUSE -- set on receiving QUSE, in beginning of qUseMsg
|
||||
| SSSQueueingQTEST -- set in qUseMsg before queuing QTEST
|
||||
| SSSSentQTEST -- set in runSmpQueueMsgDelivery after receiving Right in response to sending QTEST
|
||||
|
||||
-- after processing AM_QTEST_ in runSmpQueueMsgDelivery, snd_switch_status is set back to NULL
|
||||
```
|
||||
|
||||
In case of send it is enough to know that "snd switch is in progress", as sender cannot do any actions with recipient's switch, so status granularity is not necessary, but it can be useful for debugging and UI representation.
|
||||
|
||||
---
|
||||
Problem:
|
||||
If permanent errors in the new queue prevent switch from completing, it will be stuck forever and repeat switch will not be allowed. How to abort switch in this case while switch is in progress?
|
||||
|
||||
Solution:
|
||||
- In case of a permanent error on recipient side, delete queue (deleteQueue on server, catching errors; deleteConnRcvQueue in database), reset rcv_switch_status to NULL. However, if rcv_switch_status is one of RSSMessageReceived, RSSQueueingDelete, RSSDeleteStarted, it means that sender has already deleted the original queue and will not be able to revert, so the recipient has to complete switch regardless of permanent errors.
|
||||
- In case of a permanent error on sender side (e.g. AUTH error when trying to send QTEST), recipient has no way of knowing switch will never complete - one option is for sender to send a new message "QERR SMPQueueInfo" ( / QFAIL) in the original queue and delete queue from database; after receiving QERR recipient tries to delete queue on server, deletes queue in database, resets rcv_switch_status to NULL.
|
||||
|
||||
A new SPFailed SwitchPhase might be required for notifying client, so that switch failure is visible in client as a new chat item.
|
||||
|
||||
In case this new QERR message would have to be added anyway - why allow stopping/re-triggering switch at all? Sender agent could send QERR on permanent error when trying to send QKEY as well. This would allow to reduce number or required statuses, and remove the need to introduce stopSwitchConnection api / stop logic in agent.
|
||||
---
|
||||
|
||||
Type to communicate switch status to chat and UI:
|
||||
|
||||
```haskell
|
||||
-- rename:
|
||||
-- ConnectionStats -> ConnectionInfo,
|
||||
-- getConnectionServers -> getConnectionInfo
|
||||
|
||||
data ConnectionInfo = ConnectionInfo
|
||||
{ rcvServers :: [SMPServer],
|
||||
sndServers :: [SMPServer],
|
||||
rcvSwitchStatus :: Maybe RcvSwitchStatus,
|
||||
sndSwitchStatus :: Maybe SndSwitchStatus
|
||||
}
|
||||
```
|
||||
|
||||
Existing connections:
|
||||
|
||||
- Should switch be allowed for existing connections with switch in progress?
|
||||
- If switch should not be allowed, how to distinguish whether switch is in progress - by connection having multiple queues?
|
||||
- If yes, how to account for connections with extra queues from unfinished switches?
|
||||
|
||||
It's the easiest to allow switch for existing connections with switch already in progress and/or multiple switching queues.It is currently allowed, so it's not breaking anything new.
|
||||
@@ -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() {
|
||||
|
||||
@@ -374,6 +374,7 @@ test-suite simplex-chat-test
|
||||
MobileTests
|
||||
ProtocolTests
|
||||
SchemaDump
|
||||
ViewTests
|
||||
WebRTCTests
|
||||
Paths_simplex_chat
|
||||
hs-source-dirs:
|
||||
|
||||
+19
-17
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 =
|
||||
|
||||
@@ -8,6 +8,7 @@ import ProtocolTests
|
||||
import SchemaDump
|
||||
import Test.Hspec
|
||||
import UnliftIO.Temporary (withTempDirectory)
|
||||
import ViewTests
|
||||
import WebRTCTests
|
||||
|
||||
main :: IO ()
|
||||
@@ -15,6 +16,7 @@ main = do
|
||||
setLogLevel LogError -- LogDebug
|
||||
withGlobalLogging logCfg . hspec $ do
|
||||
describe "SimpleX chat markdown" markdownTests
|
||||
describe "SimpleX chat view" viewTests
|
||||
describe "SimpleX chat protocol" protocolTests
|
||||
describe "WebRTC encryption" webRTCTests
|
||||
describe "Schema dump" schemaDumpTest
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module ViewTests where
|
||||
|
||||
import Data.Time
|
||||
import Simplex.Chat.View
|
||||
import Test.Hspec
|
||||
|
||||
viewTests :: Spec
|
||||
viewTests = do
|
||||
testRecent
|
||||
|
||||
testRecent :: Spec
|
||||
testRecent = describe "recent" $ do
|
||||
let tz = hoursToTimeZone 1
|
||||
now1159 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 10 * 3600 + 59 * 60) -- 11:59 in tz
|
||||
now1200 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 11 * 3600) -- 12:00 in tz
|
||||
today0000 = UTCTime (fromGregorian 2023 6 6) (secondsToDiffTime $ 23 * 3600) -- 00:00 in tz
|
||||
today0600 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 5 * 3600) -- 06:00 in tz
|
||||
today1200 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 11 * 3600) -- 12:00 in tz
|
||||
today1800 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 17 * 3600) -- 18:00 in tz
|
||||
today2359 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 22 * 3600 + 59 * 60) -- 23:59 in tz
|
||||
yesterday0000 = UTCTime (fromGregorian 2023 6 5) (secondsToDiffTime $ 23 * 3600) -- 00:00 in tz
|
||||
yesterday1759 = UTCTime (fromGregorian 2023 6 6) (secondsToDiffTime $ 16 * 3600 + 59 * 60) -- 17:59 in tz
|
||||
yesterday1800 = UTCTime (fromGregorian 2023 6 6) (secondsToDiffTime $ 17 * 3600) -- 18:00 in tz
|
||||
yesterday2359 = UTCTime (fromGregorian 2023 6 6) (secondsToDiffTime $ 22 * 3600 + 59 * 60) -- 23:59 in tz
|
||||
sameDayLastMonth1900 = UTCTime (fromGregorian 2023 5 7) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
prevDayLastMonth1900 = UTCTime (fromGregorian 2023 5 6) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
sameDayLastYear1900 = UTCTime (fromGregorian 2022 6 7) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
prevDayLastYear1900 = UTCTime (fromGregorian 2022 6 6) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
tomorrow0000 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 23 * 3600) -- 00:00 in tz
|
||||
tomorrow1759 = UTCTime (fromGregorian 2023 6 8) (secondsToDiffTime $ 16 * 3600 + 59 * 60) -- 17:59 in tz
|
||||
tomorrow1800 = UTCTime (fromGregorian 2023 6 8) (secondsToDiffTime $ 17 * 3600) -- 18:00 in tz
|
||||
tomorrow2359 = UTCTime (fromGregorian 2023 6 8) (secondsToDiffTime $ 22 * 3600 + 59 * 60) -- 23:59 in tz
|
||||
sameDayNextMonth1900 = UTCTime (fromGregorian 2023 7 7) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
prevDayNextMonth1900 = UTCTime (fromGregorian 2023 7 6) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
sameDayNextYear1900 = UTCTime (fromGregorian 2024 6 7) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
prevDayNextYear1900 = UTCTime (fromGregorian 2024 6 6) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz
|
||||
test tz now1159 today0000 True
|
||||
test tz now1159 today0600 True
|
||||
test tz now1159 today1200 True
|
||||
test tz now1159 today1800 True
|
||||
test tz now1159 today2359 True
|
||||
test tz now1159 yesterday0000 False
|
||||
test tz now1159 yesterday1759 False
|
||||
test tz now1159 yesterday1800 True
|
||||
test tz now1159 yesterday2359 True
|
||||
test tz now1159 sameDayLastMonth1900 False
|
||||
test tz now1159 prevDayLastMonth1900 False
|
||||
test tz now1159 sameDayLastYear1900 False
|
||||
test tz now1159 prevDayLastYear1900 False
|
||||
test tz now1159 tomorrow0000 False
|
||||
test tz now1159 tomorrow1759 False
|
||||
test tz now1159 tomorrow1800 False
|
||||
test tz now1159 tomorrow2359 False
|
||||
test tz now1159 sameDayNextMonth1900 False
|
||||
test tz now1159 prevDayNextMonth1900 False
|
||||
test tz now1159 sameDayNextYear1900 False
|
||||
test tz now1159 prevDayNextYear1900 False
|
||||
|
||||
test tz now1200 today0000 True
|
||||
test tz now1200 today0600 True
|
||||
test tz now1200 today1200 True
|
||||
test tz now1200 today1800 True
|
||||
test tz now1200 today2359 True
|
||||
test tz now1200 yesterday0000 False
|
||||
test tz now1200 yesterday1759 False
|
||||
test tz now1200 yesterday1800 False
|
||||
test tz now1200 yesterday2359 False
|
||||
test tz now1200 sameDayLastMonth1900 False
|
||||
test tz now1200 prevDayLastMonth1900 False
|
||||
test tz now1200 sameDayLastYear1900 False
|
||||
test tz now1200 prevDayLastYear1900 False
|
||||
test tz now1200 tomorrow0000 False
|
||||
test tz now1200 tomorrow1759 False
|
||||
test tz now1200 tomorrow1800 False
|
||||
test tz now1200 tomorrow2359 False
|
||||
test tz now1200 sameDayNextMonth1900 False
|
||||
test tz now1200 prevDayNextMonth1900 False
|
||||
test tz now1200 sameDayNextYear1900 False
|
||||
test tz now1200 prevDayNextYear1900 False
|
||||
where
|
||||
test tz now time expected =
|
||||
it ("returns " <> show expected <> " for time " <> show time <> " when time zone is " <> show tz <> " and current time is " <> show now) $
|
||||
recent now tz time `shouldBe` expected
|
||||
@@ -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>
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
layout: layouts/rightscon.html
|
||||
title: "SimpleX Chat - RightsCon group"
|
||||
description: "Join the group of attendees of RightsCon 2023"
|
||||
groupLink: "https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FyHKMxr06RLUiKent0IREl1rwUtsc1MKs%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAwxplfiUKydkqy7Rbl-YQCWUSnrV_ADSd5fWvH17BvEs%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22B3dC9QiKk4AEpGWaLUuPxw%3D%3D%22%7D"
|
||||
templateEngineOverride: njk
|
||||
---
|
||||
Reference in New Issue
Block a user