Compare commits
1 Commits
v4.3.2
...
_archived-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e099d08325 |
@@ -248,7 +248,6 @@ It is possible to donate via:
|
||||
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
|
||||
- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
|
||||
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
||||
- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
||||
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
|
||||
- please let us know, via GitHub issue or chat, if you want to create a donation in some other cryptocurrency - we will add the address to the list.
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "chat.simplex.app"
|
||||
minSdk 29
|
||||
targetSdk 32
|
||||
versionCode 78
|
||||
versionName "4.3.2"
|
||||
versionCode 77
|
||||
versionName "4.3.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
ndk {
|
||||
|
||||
@@ -11,7 +11,8 @@ import chat.simplex.app.views.onboarding.OnboardingStage
|
||||
import chat.simplex.app.views.usersettings.NotificationsMode
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import java.io.*
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.util.*
|
||||
import java.util.concurrent.Semaphore
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -172,18 +173,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
val s = Semaphore(0)
|
||||
thread(name="stdout/stderr pipe") {
|
||||
Log.d(TAG, "starting server")
|
||||
var server: LocalServerSocket? = null
|
||||
for (i in 0..100) {
|
||||
try {
|
||||
server = LocalServerSocket(socketName + i)
|
||||
break
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
if (server == null) {
|
||||
throw Error("Unable to setup local server socket. Contact developers")
|
||||
}
|
||||
val server = LocalServerSocket(socketName)
|
||||
Log.d(TAG, "started server")
|
||||
s.release()
|
||||
val receiver = server.accept()
|
||||
|
||||
@@ -1259,7 +1259,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 101;
|
||||
CURRENT_PROJECT_VERSION = 100;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 4.3.2;
|
||||
MARKETING_VERSION = 4.3.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1301,7 +1301,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 101;
|
||||
CURRENT_PROJECT_VERSION = 100;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1322,7 +1322,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 4.3.2;
|
||||
MARKETING_VERSION = 4.3.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1380,7 +1380,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 101;
|
||||
CURRENT_PROJECT_VERSION = 100;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1393,7 +1393,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 4.3.2;
|
||||
MARKETING_VERSION = 4.3.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1410,7 +1410,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 101;
|
||||
CURRENT_PROJECT_VERSION = 100;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1423,7 +1423,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 4.3.2;
|
||||
MARKETING_VERSION = 4.3.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
208
docs/rfcs/2022-12-09-ephemeral-conversations.md
Normal file
208
docs/rfcs/2022-12-09-ephemeral-conversations.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Ephemeral conversations with existing contacts
|
||||
|
||||
Ephemeral conversation inside existing conversation with stricter security properties.
|
||||
|
||||
- additional level of encryption for message bodies & text, chat items content & text (more?) inside chat db
|
||||
|
||||
- ephemeral conversation key not persisted - only stored in-memory of current chat session, cleared on exiting to background
|
||||
|
||||
- separate tables for chat items and messages to avoid gaps in ids? TBC db pages on deletion
|
||||
|
||||
- don't persist chat items and messages at all? if yes - how to support multiple ephemeral conversations with different contacts in the same session - holding chat items in memory may become expensive. though only "not yet seen" items may have to be held in memory - after opening ephemeral conversation no longer keep them in memory; in this case closing ephemeral conversation screen (not fully exiting but keeping it in list of current ephemeral conversations) and re-opening also does not restore chat items, though connection and key are preserved
|
||||
|
||||
- if multiple ephemeral conversations are allowed - how to know they have new messages - should there be notifications for them? only local or push notifications too? not a regular notification but indication in chat list?
|
||||
|
||||
- disabled features? e.g. "reply" if messages aren't persisted. voice messages, files, etc.? if files are supported they are deleted exiting, should also be a part of chat start cleanup process (see below)
|
||||
|
||||
- contact is required to be verified to start ephemeral conversation - improves guarantee that the key for ephemeral conversation is agreed in a secure context
|
||||
|
||||
- no visibility of contact profile in UI
|
||||
|
||||
- separated with a blank screen / transition from a main conversation to prevent them appearing in the same screen
|
||||
|
||||
- new entity - not a contact?
|
||||
|
||||
- new chat type & direction, or additional dimension?
|
||||
|
||||
- api to start new and open existing (limited by chat session lifespan) ephemeral conversation
|
||||
|
||||
- api to join - also requires verified connection? one party can have contact verified, second not - prohibit until verified?
|
||||
|
||||
- join via special chat item? join via same button that is used to start? allow both? chat item and negotiation messages should be automatically deleted on end or on cleanup
|
||||
|
||||
- api to end - any side can initiate, both sides client cooperate and delete?
|
||||
|
||||
- a new connection is created for the conversation, deleted upon end, incognito mode doesn't affect - no profile is shared at all
|
||||
|
||||
- on chat start - deletes ephemeral conversations that were not ended, due to crash or another reason (get synchronously before starting?)
|
||||
|
||||
- controller has state of all "active" ephemeral conversations, saved are not loaded - what if one party crashes and not ends, then creates a new ephemeral conversation - previous is ended for another party?
|
||||
|
||||
## Design
|
||||
|
||||
\***
|
||||
|
||||
Track current ephemeral conversations in ChatController.
|
||||
|
||||
``` haskell
|
||||
data ChatController = ChatController {
|
||||
...
|
||||
currentECs :: TMap ContactId EphemeralConversation
|
||||
...
|
||||
}
|
||||
|
||||
data EphemeralConversation = EphemeralConversation
|
||||
{ chatItemId :: Int64,
|
||||
ecState :: ECState
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data ECState
|
||||
= ECInvitationSent { localDhPrivKey :: C.PrivateKeyX25519 }
|
||||
| ECInvitationReceived { localDhPubKey :: C.PublicKeyX25519 }
|
||||
| ECAcptSent { sharedKey :: Maybe C.Key }
|
||||
| ECAcptReceived { sharedKey :: Maybe C.Key }
|
||||
| ECNegotiated { sharedKey :: Maybe C.Key }
|
||||
|
||||
data ECStateTag
|
||||
= ECSTInvitationSent
|
||||
| ECSTInvitationReceived
|
||||
| ECSTAcptSent
|
||||
| ECSTAcptReceived
|
||||
| ECSTNegotiated
|
||||
|
||||
ecStateTag :: ECState -> ECStateTag
|
||||
```
|
||||
|
||||
\***
|
||||
|
||||
Protocol messages:
|
||||
|
||||
- `XECInv C.PublicKeyX25519` - invite to ephemeral conversation, other properties except key? ECInvitation type to contain properties?
|
||||
|
||||
- on send: add to Controller's `currentECs` in state `ECInvitationSent` and create `CIECInvitation` chat item.
|
||||
|
||||
- on receive: `processXECInv` - add to Controller's `currentECs` in state `ECInvitationReceived` and create `CIECInvitation` chat item.
|
||||
|
||||
- `XECAcpt C.PublicKeyX25519 ConnReqInvitation` - accept ephemeral conversation, send link to join.
|
||||
|
||||
- on send: update Controller's `currentECs` record to state `ECAcptSent`, update chat item.
|
||||
|
||||
- on receive: `processXECAcpt` - update Controller's `currentECs` record to state `ECAcptReceived`, update chat item.
|
||||
|
||||
- `XECEnd` - message to end ephemeral conversation. Send in main connection or new one? Main may be better as it may signal cancel as well if ephemeral conversation wasn't yet accepted/negotiated.
|
||||
|
||||
Race condition if both parties send `XECInv` simultaneously - if `XECInv` is received when there is ephemeral conversation in `currentECs` in state `ECInvitationSent`, just remove it and signal error `CEECNegotiationError`.
|
||||
|
||||
\***
|
||||
|
||||
APIs:
|
||||
|
||||
- `APIStartEC ContactId` - sends `XECInv`, in UI ephemeral chat view is opened, disabled/progress indicator until ephemeral conversation is negotiated.
|
||||
- `APIJoinEC ContactId` - sends `XECAcpt`, in UI ephemeral chat view is opened, disabled/progress indicator until ephemeral conversation is negotiated.
|
||||
- `APIOpenEC ContactId` - loads chat items (?) for current ephemeral conversation, opens ephemeral chat view.
|
||||
- api to reject? or just allow to delete chat item?
|
||||
- `APIEndEC ContactId` - sends `XECEnd`, deletes connection, ephemeral conversation entity and chat items, removes from `currentECs` state, deletes `CIECInvitation` chat item, in UI chat is closed.
|
||||
- terminal counterparts
|
||||
|
||||
ChatResponses (mirroring chat item updates for terminal):
|
||||
|
||||
- `CRECInvitationSent {contact :: Contact}`
|
||||
- `CRECInvitationReceived {contact :: Contact}`
|
||||
- `CRECAccepted {contact :: Contact}`
|
||||
- `CRECAcceptReceived {contact :: Contact}`
|
||||
- `CRECEnded {contact :: Contact}`
|
||||
|
||||
ChatErrors:
|
||||
|
||||
- `CEECNegotiationError {contactId :: ContactId}` - both sent `XECInv`, failed to establish connection, etc.
|
||||
- `CENoCurrentEC` - on trying to open, accept, end.
|
||||
- `CEECState {currentECState :: ECStateTag}` - on state errors
|
||||
|
||||
\***
|
||||
|
||||
Chat item content:
|
||||
|
||||
``` haskell
|
||||
data CIContent (d :: MsgDirection) where
|
||||
...
|
||||
CIRcvECInvitation ECStateTag -> CIContent 'MDRcv
|
||||
CISndECInvitation ECStateTag -> CIContent 'MDSnd
|
||||
...
|
||||
```
|
||||
|
||||
is there a need for more detailed `CIECStatus` or state tag will suffice? (see CICallStatus)
|
||||
|
||||
\***
|
||||
|
||||
New chat type, direction, chat info:
|
||||
|
||||
``` haskell
|
||||
data ChatType = ... | CTEphemeral ...
|
||||
|
||||
-- new ChatType requires processing cases on ChatRef in all APIs based on it, which may be a good thing
|
||||
-- e.g. automatically requires separate APISendMessage api
|
||||
|
||||
data ChatInfo (c :: ChatType) where
|
||||
...
|
||||
EphemeralChat :: ChatInfo 'CTEphemeral -- no additional information required?
|
||||
...
|
||||
|
||||
data CIDirection (c :: ChatType) (d :: MsgDirection) where
|
||||
...
|
||||
CIEphemeralSnd :: CIDirection 'CTEphemeral 'MDSnd
|
||||
CIEphemeralRcv :: CIDirection 'CTEphemeral 'MDRcv
|
||||
...
|
||||
|
||||
-- same for `data CIQDirection (c :: ChatType)`
|
||||
|
||||
-- same for `data SChatType (c :: ChatType)`
|
||||
```
|
||||
|
||||
Maybe it should be a new dimension and not ChatType, though it may have to be drastically different for groups and easier expressed as a separate `CTEphemeralGroup` ChatType.
|
||||
|
||||
Pros of not having it as contact's flag/dimension:
|
||||
|
||||
- no special casing in loading chat previews
|
||||
- no special casing in APIs, instead it's fully fledged ChatType
|
||||
- separate table for entity - cleaner deletion, no gaps
|
||||
- can have separate ConnectionEntity, though it may be a con
|
||||
|
||||
\***
|
||||
|
||||
New ConnectionEntity?
|
||||
|
||||
- `RcvDirectEphemeralMsgConnection {entityConnection :: Connection}`
|
||||
|
||||
can allow to easily prohibit many protocol messages, e.g. calls, groups, etc.
|
||||
|
||||
\***
|
||||
|
||||
Database changes:
|
||||
|
||||
```sql
|
||||
CREATE TABLE ephemeral_conversations(
|
||||
ephemeral_conversation_id INTEGER PRIMARY KEY,
|
||||
contact_id INTEGER REFERENCES contacts(contact_id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT CHECK(updated_at NOT NULL)
|
||||
);
|
||||
|
||||
-- separate ec_messages and ec_chat_items tables?
|
||||
|
||||
-- or foreign_keys to ephemeral_conversations in existing messages and chat_items tables?
|
||||
|
||||
-- if files allowed: same question
|
||||
|
||||
-- don't save chat items and messages at all? see above. if it's separate ConnectionEntity it's not hard
|
||||
|
||||
ALTER TABLE connections ADD COLUMN ephemeral_conversation_id INTEGER DEFAULT NULL
|
||||
REFERENCES ephemeral_conversations (ephemeral_conversation_id) ON DELETE CASCADE;
|
||||
|
||||
-- add logic on loading entities, e.g. for subscriptions
|
||||
```
|
||||
|
||||
If tables for chat items and messages are separate - logic for saving encrypted message/item content, text, etc. doesn't affect existing queries and code. If it's a new connection entity type it's separate cases in api/processing anyway.
|
||||
|
||||
If tables for chat items and messages are reused - To/FromField don't auto convert using To/FromJSON, instead saved/loaded as string, decrypted based on flag?
|
||||
@@ -1,5 +1,5 @@
|
||||
name: simplex-chat
|
||||
version: 4.3.2
|
||||
version: 4.3.1
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
|
||||
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 4.3.2
|
||||
version: 4.3.1
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
@@ -67,7 +67,6 @@ library
|
||||
Simplex.Chat.Migrations.M20221130_delete_item_deleted
|
||||
Simplex.Chat.Migrations.M20221209_verified_connection
|
||||
Simplex.Chat.Migrations.M20221210_idxs
|
||||
Simplex.Chat.Migrations.M20221211_group_description
|
||||
Simplex.Chat.Mobile
|
||||
Simplex.Chat.Options
|
||||
Simplex.Chat.ProfileGenerator
|
||||
|
||||
@@ -1020,12 +1020,9 @@ processChatCommand = \case
|
||||
APIUpdateGroupProfile groupId p' -> withUser $ \user -> do
|
||||
g <- withStore $ \db -> getGroup db user groupId
|
||||
runUpdateGroupProfile user g p'
|
||||
UpdateGroupNames gName GroupProfile {displayName, fullName} ->
|
||||
updateGroupProfileByName gName $ \p -> p {displayName, fullName}
|
||||
ShowGroupProfile gName -> withUser $ \user ->
|
||||
CRGroupProfile <$> withStore (\db -> getGroupInfoByName db user gName)
|
||||
UpdateGroupDescription gName description ->
|
||||
updateGroupProfileByName gName $ \p -> p {description}
|
||||
UpdateGroupProfile gName profile -> withUser $ \user -> do
|
||||
groupId <- withStore $ \db -> getGroupIdByName db user gName
|
||||
processChatCommand $ APIUpdateGroupProfile groupId profile
|
||||
APICreateGroupLink groupId -> withUser $ \user -> withChatLock "createGroupLink" $ do
|
||||
gInfo@GroupInfo {membership = membership@GroupMember {memberRole = userRole}} <- withStore $ \db -> getGroupInfo db user groupId
|
||||
when (userRole < GRAdmin) $ throwChatError CEGroupUserRole
|
||||
@@ -1120,9 +1117,10 @@ processChatCommand = \case
|
||||
ct@Contact {userPreferences} <- withStore $ \db -> getContactByName db user cName
|
||||
let prefs' = setPreference f allowed_ $ Just userPreferences
|
||||
updateContactPrefs user ct prefs'
|
||||
SetGroupFeature f gName enabled ->
|
||||
updateGroupProfileByName gName $ \p ->
|
||||
p {groupPreferences = Just . setGroupPreference f enabled $ groupPreferences p}
|
||||
SetGroupFeature f gName enabled -> withUser $ \user -> do
|
||||
g@(Group GroupInfo {groupProfile = p} _) <- withStore $ \db -> getGroup db user =<< getGroupIdByName db user gName
|
||||
let p' = p {groupPreferences = Just . setGroupPreference f enabled $ groupPreferences p}
|
||||
runUpdateGroupProfile user g p'
|
||||
QuitChat -> liftIO exitSuccess
|
||||
ShowVersion -> pure $ CRVersionInfo versionNumber
|
||||
DebugLocks -> do
|
||||
@@ -1166,16 +1164,12 @@ processChatCommand = \case
|
||||
getGroupAndMemberId user gName mName >>= processChatCommand . uncurry cmd
|
||||
getConnectionCode :: ConnId -> m Text
|
||||
getConnectionCode connId = verificationCode <$> withAgent (`getConnectionRatchetAdHash` connId)
|
||||
verifyConnectionCode :: User -> Connection -> Maybe Text -> m ChatResponse
|
||||
verifyConnectionCode user conn@Connection {connId} (Just code) = do
|
||||
verifyConnectionCode :: User -> Connection -> Text -> m ChatResponse
|
||||
verifyConnectionCode user conn@Connection {connId} code = do
|
||||
code' <- getConnectionCode $ aConnId conn
|
||||
let verified = sameVerificationCode code code'
|
||||
when verified . withStore' $ \db -> setConnectionVerified db user connId $ Just code'
|
||||
pure $ CRConnectionVerified verified code'
|
||||
verifyConnectionCode user conn@Connection {connId} _ = do
|
||||
code' <- getConnectionCode $ aConnId conn
|
||||
withStore' $ \db -> setConnectionVerified db user connId Nothing
|
||||
pure $ CRConnectionVerified False code'
|
||||
pure $ CRCodeVerification verified code'
|
||||
getSentChatItemIdByText :: User -> ChatRef -> ByteString -> m Int64
|
||||
getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of
|
||||
CTDirect -> withStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd (safeDecodeUtf8 msg)
|
||||
@@ -1263,11 +1257,6 @@ processChatCommand = \case
|
||||
toView . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat g') ci
|
||||
createGroupFeatureChangedItems user cd CISndGroupFeature p p'
|
||||
pure $ CRGroupUpdated g g' Nothing
|
||||
updateGroupProfileByName :: GroupName -> (GroupProfile -> GroupProfile) -> m ChatResponse
|
||||
updateGroupProfileByName gName update = withUser $ \user -> do
|
||||
g@(Group GroupInfo {groupProfile = p} _) <- withStore $ \db ->
|
||||
getGroupIdByName db user gName >>= getGroup db user
|
||||
runUpdateGroupProfile user g $ update p
|
||||
isReady :: Contact -> Bool
|
||||
isReady ct =
|
||||
let s = connStatus $ activeConn (ct :: Contact)
|
||||
@@ -1968,9 +1957,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
||||
GCHostMember -> do
|
||||
toView $ CRUserJoinedGroup gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected}
|
||||
createGroupFeatureItems gInfo m
|
||||
let GroupInfo {groupProfile = GroupProfile {description}} = gInfo
|
||||
memberConnectedChatItem gInfo m
|
||||
forM_ description $ groupDescriptionChatItem gInfo m
|
||||
setActive $ ActiveG gName
|
||||
showToast ("#" <> gName) "you are connected to group"
|
||||
GCInviteeMember -> do
|
||||
@@ -2246,10 +2233,6 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
||||
-- ts should be broker ts but we don't have it for CON
|
||||
createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEMemberConnected) Nothing
|
||||
|
||||
groupDescriptionChatItem :: GroupInfo -> GroupMember -> Text -> m ()
|
||||
groupDescriptionChatItem gInfo m descr =
|
||||
createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvMsgContent $ MCText descr) Nothing
|
||||
|
||||
notifyMemberConnected :: GroupInfo -> GroupMember -> m ()
|
||||
notifyMemberConnected gInfo m@GroupMember {localDisplayName = c} = do
|
||||
memberConnectedChatItem gInfo m
|
||||
@@ -3395,7 +3378,7 @@ chatCommandP =
|
||||
"/_accept " *> (APIAcceptContact <$> A.decimal),
|
||||
"/_reject " *> (APIRejectContact <$> A.decimal),
|
||||
"/_call invite @" *> (APISendCallInvitation <$> A.decimal <* A.space <*> jsonP),
|
||||
"/call " *> char_ '@' *> (SendCallInvitation <$> displayName <*> pure defaultCallType),
|
||||
("/call @" <|> "/call ") *> (SendCallInvitation <$> displayName <*> pure defaultCallType),
|
||||
"/_call reject @" *> (APIRejectCall <$> A.decimal),
|
||||
"/_call offer @" *> (APISendCallOffer <$> A.decimal <* A.space <*> jsonP),
|
||||
"/_call answer @" *> (APISendCallAnswer <$> A.decimal <* A.space <*> jsonP),
|
||||
@@ -3437,43 +3420,42 @@ chatCommandP =
|
||||
"/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP),
|
||||
"/_info #" *> (APIGroupMemberInfo <$> A.decimal <* A.space <*> A.decimal),
|
||||
"/_info @" *> (APIContactInfo <$> A.decimal),
|
||||
("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* char_ '@' <*> displayName),
|
||||
("/info " <|> "/i ") *> char_ '@' *> (ContactInfo <$> displayName),
|
||||
("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* optional (A.char '@') <*> displayName),
|
||||
("/info @" <|> "/info " <|> "/i @" <|> "/i ") *> (ContactInfo <$> displayName),
|
||||
"/_switch #" *> (APISwitchGroupMember <$> A.decimal <* A.space <*> A.decimal),
|
||||
"/_switch @" *> (APISwitchContact <$> A.decimal),
|
||||
"/switch #" *> (SwitchGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName),
|
||||
"/switch " *> char_ '@' *> (SwitchContact <$> displayName),
|
||||
"/switch #" *> (SwitchGroupMember <$> displayName <* A.space <* optional (A.char '@') <*> displayName),
|
||||
("/switch @" <|> "/switch ") *> (SwitchContact <$> displayName),
|
||||
"/_get code @" *> (APIGetContactCode <$> A.decimal),
|
||||
"/_get code #" *> (APIGetGroupMemberCode <$> A.decimal <* A.space <*> A.decimal),
|
||||
"/_verify code @" *> (APIVerifyContact <$> A.decimal <*> optional (A.space *> textP)),
|
||||
"/_verify code #" *> (APIVerifyGroupMember <$> A.decimal <* A.space <*> A.decimal <*> optional (A.space *> textP)),
|
||||
"/code " *> char_ '@' *> (GetContactCode <$> displayName),
|
||||
"/code #" *> (GetGroupMemberCode <$> displayName <* A.space <* char_ '@' <*> displayName),
|
||||
"/verify " *> char_ '@' *> (VerifyContact <$> displayName <*> optional (A.space *> textP)),
|
||||
"/verify #" *> (VerifyGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> optional (A.space *> textP)),
|
||||
"/_verify code @" *> (APIVerifyContact <$> A.decimal <* A.space <*> textP),
|
||||
"/_verify code @" *> (APIVerifyGroupMember <$> A.decimal <* A.space <*> A.decimal <* A.space <*> textP),
|
||||
("/code @" <|> "/code ") *> (GetContactCode <$> displayName),
|
||||
"/code #" *> (GetGroupMemberCode <$> displayName <* A.space <* optional (A.char '@') <*> displayName),
|
||||
("/verify @" <|> "/verify ") *> (VerifyContact <$> displayName <* A.space <*> textP),
|
||||
"/verify #" *> (VerifyGroupMember <$> displayName <* A.space <* optional (A.char '@') <*> displayName <* A.space <*> textP),
|
||||
("/help files" <|> "/help file" <|> "/hf") $> ChatHelp HSFiles,
|
||||
("/help groups" <|> "/help group" <|> "/hg") $> ChatHelp HSGroups,
|
||||
("/help address" <|> "/ha") $> ChatHelp HSMyAddress,
|
||||
("/help messages" <|> "/hm") $> ChatHelp HSMessages,
|
||||
("/help settings" <|> "/hs") $> ChatHelp HSSettings,
|
||||
("/help" <|> "/h") $> ChatHelp HSMain,
|
||||
("/group " <|> "/g ") *> char_ '#' *> (NewGroup <$> groupProfile),
|
||||
("/group #" <|> "/group " <|> "/g #" <|> "/g ") *> (NewGroup <$> groupProfile),
|
||||
"/_group " *> (NewGroup <$> jsonP),
|
||||
("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole),
|
||||
("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayName),
|
||||
("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole),
|
||||
("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMember <$> displayName <* A.space <* char_ '@' <*> displayName),
|
||||
("/leave " <|> "/l ") *> char_ '#' *> (LeaveGroup <$> displayName),
|
||||
("/add #" <|> "/add " <|> "/a #" <|> "/a ") *> (AddMember <$> displayName <* A.space <* optional (A.char '@') <*> displayName <*> memberRole),
|
||||
("/join #" <|> "/join " <|> "/j #" <|> "/j ") *> (JoinGroup <$> displayName),
|
||||
("/member role #" <|> "/member role " <|> "/mr #" <|> "/mr ") *> (MemberRole <$> displayName <* A.space <* optional (A.char '@') <*> displayName <*> memberRole),
|
||||
("/remove #" <|> "/remove " <|> "/rm #" <|> "/rm ") *> (RemoveMember <$> displayName <* A.space <* optional (A.char '@') <*> displayName),
|
||||
("/leave #" <|> "/leave " <|> "/l #" <|> "/l ") *> (LeaveGroup <$> displayName),
|
||||
("/delete #" <|> "/d #") *> (DeleteGroup <$> displayName),
|
||||
("/delete " <|> "/d ") *> char_ '@' *> (DeleteContact <$> displayName),
|
||||
("/delete @" <|> "/delete " <|> "/d @" <|> "/d ") *> (DeleteContact <$> displayName),
|
||||
"/clear #" *> (ClearGroup <$> displayName),
|
||||
"/clear " *> char_ '@' *> (ClearContact <$> displayName),
|
||||
("/members " <|> "/ms ") *> char_ '#' *> (ListMembers <$> displayName),
|
||||
("/clear @" <|> "/clear ") *> (ClearContact <$> displayName),
|
||||
("/members #" <|> "/members " <|> "/ms #" <|> "/ms ") *> (ListMembers <$> displayName),
|
||||
("/groups" <|> "/gs") $> ListGroups,
|
||||
"/_group_profile #" *> (APIUpdateGroupProfile <$> A.decimal <* A.space <*> jsonP),
|
||||
("/group_profile " <|> "/gp ") *> char_ '#' *> (UpdateGroupNames <$> displayName <* A.space <*> groupProfile),
|
||||
("/group_profile " <|> "/gp ") *> char_ '#' *> (ShowGroupProfile <$> displayName),
|
||||
"/group_descr " *> char_ '#' *> (UpdateGroupDescription <$> displayName <*> optional (A.space *> (jsonP <|> textP))),
|
||||
-- TODO group profile update via terminal should not reset image and preferences to Nothing (now it does)
|
||||
("/group_profile #" <|> "/gp #" <|> "/group_profile " <|> "/gp ") *> (UpdateGroupProfile <$> displayName <* A.space <*> groupProfile),
|
||||
"/_create link #" *> (APICreateGroupLink <$> A.decimal),
|
||||
"/_delete link #" *> (APIDeleteGroupLink <$> A.decimal),
|
||||
"/_get link #" *> (APIGetGroupLink <$> A.decimal),
|
||||
@@ -3481,7 +3463,7 @@ chatCommandP =
|
||||
"/delete link #" *> (DeleteGroupLink <$> displayName),
|
||||
"/show link #" *> (ShowGroupLink <$> displayName),
|
||||
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <*> pure Nothing <*> quotedMsg <*> A.takeByteString),
|
||||
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <* char_ '@' <*> (Just <$> displayName) <* A.space <*> quotedMsg <*> A.takeByteString),
|
||||
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <* optional (A.char '@') <*> (Just <$> displayName) <* A.space <*> quotedMsg <*> A.takeByteString),
|
||||
("/contacts" <|> "/cs") $> ListContacts,
|
||||
("/connect " <|> "/c ") *> (Connect <$> ((Just <$> strP) <|> A.takeByteString $> Nothing)),
|
||||
("/connect" <|> "/c") $> AddContact,
|
||||
@@ -3505,8 +3487,8 @@ chatCommandP =
|
||||
("/delete_address" <|> "/da") $> DeleteMyAddress,
|
||||
("/show_address" <|> "/sa") $> ShowMyAddress,
|
||||
"/auto_accept " *> (AddressAutoAccept <$> autoAcceptP),
|
||||
("/accept " <|> "/ac ") *> char_ '@' *> (AcceptContact <$> displayName),
|
||||
("/reject " <|> "/rc ") *> char_ '@' *> (RejectContact <$> displayName),
|
||||
("/accept @" <|> "/accept " <|> "/ac @" <|> "/ac ") *> (AcceptContact <$> displayName),
|
||||
("/reject @" <|> "/reject " <|> "/rc @" <|> "/rc ") *> (RejectContact <$> displayName),
|
||||
("/markdown" <|> "/m") $> ChatHelp HSMarkdown,
|
||||
("/welcome" <|> "/w") $> Welcome,
|
||||
"/profile_image " *> (UpdateProfileImage . Just . ImageData <$> imageP),
|
||||
@@ -3555,7 +3537,7 @@ chatCommandP =
|
||||
gName <- displayName
|
||||
fullName <- fullNameP gName
|
||||
let groupPreferences = Just (emptyGroupPrefs :: GroupPreferences) {directMessages = Just GroupPreference {enable = FEOn}}
|
||||
pure GroupProfile {displayName = gName, fullName, description = Nothing, image = Nothing, groupPreferences}
|
||||
pure GroupProfile {displayName = gName, fullName, image = Nothing, groupPreferences}
|
||||
fullNameP name = do
|
||||
n <- (A.space *> A.takeByteString) <|> pure ""
|
||||
pure $ if B.null n then name else safeDecodeUtf8 n
|
||||
@@ -3593,7 +3575,6 @@ chatCommandP =
|
||||
(Just <$> (AutoAccept <$> (" incognito=" *> onOffP <|> pure False) <*> optional (A.space *> msgContentP)))
|
||||
(pure Nothing)
|
||||
toServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = True}
|
||||
char_ = optional . A.char
|
||||
|
||||
adminContactReq :: ConnReqContact
|
||||
adminContactReq =
|
||||
|
||||
@@ -202,8 +202,8 @@ data ChatCommand
|
||||
| APISwitchGroupMember GroupId GroupMemberId
|
||||
| APIGetContactCode ContactId
|
||||
| APIGetGroupMemberCode GroupId GroupMemberId
|
||||
| APIVerifyContact ContactId (Maybe Text)
|
||||
| APIVerifyGroupMember GroupId GroupMemberId (Maybe Text)
|
||||
| APIVerifyContact ContactId Text
|
||||
| APIVerifyGroupMember GroupId GroupMemberId Text
|
||||
| ShowMessages ChatName Bool
|
||||
| ContactInfo ContactName
|
||||
| GroupMemberInfo GroupName ContactName
|
||||
@@ -211,8 +211,8 @@ data ChatCommand
|
||||
| SwitchGroupMember GroupName ContactName
|
||||
| GetContactCode ContactName
|
||||
| GetGroupMemberCode GroupName ContactName
|
||||
| VerifyContact ContactName (Maybe Text)
|
||||
| VerifyGroupMember GroupName ContactName (Maybe Text)
|
||||
| VerifyContact ContactName Text
|
||||
| VerifyGroupMember GroupName ContactName Text
|
||||
| ChatHelp HelpSection
|
||||
| Welcome
|
||||
| AddContact
|
||||
@@ -242,9 +242,7 @@ data ChatCommand
|
||||
| ClearGroup GroupName
|
||||
| ListMembers GroupName
|
||||
| ListGroups
|
||||
| UpdateGroupNames GroupName GroupProfile
|
||||
| ShowGroupProfile GroupName
|
||||
| UpdateGroupDescription GroupName (Maybe Text)
|
||||
| UpdateGroupProfile GroupName GroupProfile
|
||||
| CreateGroupLink GroupName
|
||||
| DeleteGroupLink GroupName
|
||||
| ShowGroupLink GroupName
|
||||
@@ -288,7 +286,7 @@ data ChatResponse
|
||||
| CRGroupMemberSwitch {groupInfo :: GroupInfo, member :: GroupMember, switchProgress :: SwitchProgress}
|
||||
| CRContactCode {contact :: Contact, connectionCode :: Text}
|
||||
| CRGroupMemberCode {groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text}
|
||||
| CRConnectionVerified {verified :: Bool, expectedCode :: Text}
|
||||
| CRCodeVerification {verified :: Bool, expectedCode :: Text}
|
||||
| CRNewChatItem {chatItem :: AChatItem}
|
||||
| CRChatItemStatusUpdated {chatItem :: AChatItem}
|
||||
| CRChatItemUpdated {chatItem :: AChatItem}
|
||||
@@ -370,7 +368,6 @@ data ChatResponse
|
||||
| CRGroupRemoved {groupInfo :: GroupInfo}
|
||||
| CRGroupDeleted {groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRGroupUpdated {fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember}
|
||||
| CRGroupProfile {groupInfo :: GroupInfo}
|
||||
| CRGroupLinkCreated {groupInfo :: GroupInfo, connReqContact :: ConnReqContact}
|
||||
| CRGroupLink {groupInfo :: GroupInfo, connReqContact :: ConnReqContact}
|
||||
| CRGroupLinkDeleted {groupInfo :: GroupInfo}
|
||||
|
||||
@@ -123,12 +123,9 @@ groupsHelpInfo =
|
||||
indent <> highlight "/leave <group> " <> " - leave group",
|
||||
indent <> highlight "/delete <group> " <> " - delete group",
|
||||
indent <> highlight "/members <group> " <> " - list group members",
|
||||
indent <> highlight "/gp <group> " <> " - view group profile",
|
||||
indent <> highlight "/gp <group> <new_name> [<full_name>] " <> " - update group profile",
|
||||
indent <> highlight "/group_descr <group> [<descr>] " <> " - update/remove group description",
|
||||
indent <> highlight "/groups " <> " - list groups",
|
||||
indent <> highlight "#<group> <message> " <> " - send message to group",
|
||||
indent <> highlight "/create link #<group> " <> " - create public group link",
|
||||
"",
|
||||
"The commands may be abbreviated: " <> listHighlight ["/g", "/a", "/j", "/rm", "/l", "/d", "/ms", "/gs"]
|
||||
]
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20221211_group_description where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20221211_group_description :: Query
|
||||
m20221211_group_description =
|
||||
[sql|
|
||||
ALTER TABLE group_profiles ADD COLUMN description TEXT NULL;
|
||||
|]
|
||||
@@ -116,8 +116,7 @@ CREATE TABLE group_profiles(
|
||||
updated_at TEXT CHECK(updated_at NOT NULL),
|
||||
image TEXT,
|
||||
user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE,
|
||||
preferences TEXT,
|
||||
description TEXT NULL
|
||||
preferences TEXT
|
||||
);
|
||||
CREATE TABLE groups(
|
||||
group_id INTEGER PRIMARY KEY, -- local group ID
|
||||
|
||||
@@ -307,7 +307,6 @@ import Simplex.Chat.Migrations.M20221129_delete_group_feature_items
|
||||
import Simplex.Chat.Migrations.M20221130_delete_item_deleted
|
||||
import Simplex.Chat.Migrations.M20221209_verified_connection
|
||||
import Simplex.Chat.Migrations.M20221210_idxs
|
||||
import Simplex.Chat.Migrations.M20221211_group_description
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..))
|
||||
@@ -360,8 +359,7 @@ schemaMigrations =
|
||||
("20221129_delete_group_feature_items", m20221129_delete_group_feature_items),
|
||||
("20221130_delete_item_deleted", m20221130_delete_item_deleted),
|
||||
("20221209_verified_connection", m20221209_verified_connection),
|
||||
("20221210_idxs", m20221210_idxs),
|
||||
("20221211_group_description", m20221211_group_description)
|
||||
("20221210_idxs", m20221210_idxs)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
@@ -1511,7 +1509,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
|
||||
[sql|
|
||||
SELECT
|
||||
-- GroupInfo
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
|
||||
-- GroupInfo {membership}
|
||||
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
|
||||
mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
|
||||
@@ -1612,7 +1610,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId =
|
||||
[sql|
|
||||
SELECT
|
||||
-- GroupInfo
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
|
||||
-- GroupInfo {membership}
|
||||
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
|
||||
mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
|
||||
@@ -1652,15 +1650,15 @@ updateConnectionStatus db Connection {connId} connStatus = do
|
||||
-- | creates completely new group with a single member - the current user
|
||||
createNewGroup :: DB.Connection -> TVar ChaChaDRG -> User -> GroupProfile -> ExceptT StoreError IO GroupInfo
|
||||
createNewGroup db gVar user@User {userId} groupProfile = ExceptT $ do
|
||||
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
|
||||
let GroupProfile {displayName, fullName, image, groupPreferences} = groupProfile
|
||||
fullGroupPreferences = mergeGroupPreferences groupPreferences
|
||||
currentTs <- getCurrentTime
|
||||
withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do
|
||||
groupId <- liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
|
||||
(displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs)
|
||||
"INSERT INTO group_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
|
||||
(displayName, fullName, image, userId, groupPreferences, currentTs, currentTs)
|
||||
profileId <- insertedRowId db
|
||||
DB.execute
|
||||
db
|
||||
@@ -1696,7 +1694,7 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId, activeCo
|
||||
DB.query db "SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMIT 1" (connRequest, userId)
|
||||
createGroupInvitation_ :: ExceptT StoreError IO (GroupInfo, GroupMemberId)
|
||||
createGroupInvitation_ = do
|
||||
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
|
||||
let GroupProfile {displayName, fullName, image, groupPreferences} = groupProfile
|
||||
fullGroupPreferences = mergeGroupPreferences groupPreferences
|
||||
ExceptT $
|
||||
withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do
|
||||
@@ -1704,8 +1702,8 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId, activeCo
|
||||
groupId <- liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
|
||||
(displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs)
|
||||
"INSERT INTO group_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
|
||||
(displayName, fullName, image, userId, groupPreferences, currentTs, currentTs)
|
||||
profileId <- insertedRowId db
|
||||
DB.execute
|
||||
db
|
||||
@@ -1851,7 +1849,7 @@ getUserGroupDetails db User {userId, userContactId} =
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
|
||||
SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
|
||||
mu.group_member_id, g.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status,
|
||||
mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.local_alias, pu.preferences
|
||||
FROM groups g
|
||||
@@ -1885,15 +1883,14 @@ getGroupInfoByName db user gName = do
|
||||
gId <- getGroupIdByName db user gName
|
||||
getGroupInfo db user gId
|
||||
|
||||
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe Bool, Maybe GroupPreferences, UTCTime, UTCTime) :. GroupMemberRow
|
||||
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe ImageData, Maybe ProfileId, Maybe Bool, Maybe GroupPreferences, UTCTime, UTCTime) :. GroupMemberRow
|
||||
|
||||
toGroupInfo :: Int64 -> GroupInfoRow -> GroupInfo
|
||||
toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, groupPreferences, createdAt, updatedAt) :. userMemberRow) =
|
||||
toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, image, hostConnCustomUserProfileId, enableNtfs_, groupPreferences, createdAt, updatedAt) :. userMemberRow) =
|
||||
let membership = toGroupMember userContactId userMemberRow
|
||||
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_}
|
||||
fullGroupPreferences = mergeGroupPreferences groupPreferences
|
||||
groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences}
|
||||
in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt}
|
||||
in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image, groupPreferences}, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt}
|
||||
|
||||
getGroupMember :: DB.Connection -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember
|
||||
getGroupMember db user@User {userId} groupId groupMemberId =
|
||||
@@ -2369,7 +2366,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} =
|
||||
[sql|
|
||||
SELECT
|
||||
-- GroupInfo
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
|
||||
-- GroupInfo {membership}
|
||||
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
|
||||
mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
|
||||
@@ -3351,7 +3348,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do
|
||||
[sql|
|
||||
SELECT
|
||||
-- GroupInfo
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
|
||||
-- GroupMember - membership
|
||||
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
|
||||
mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
|
||||
@@ -3717,7 +3714,7 @@ getGroupInfo db User {userId, userContactId} groupId =
|
||||
[sql|
|
||||
SELECT
|
||||
-- GroupInfo
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
|
||||
-- GroupMember - membership
|
||||
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
|
||||
mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
|
||||
@@ -3731,7 +3728,7 @@ getGroupInfo db User {userId, userContactId} groupId =
|
||||
(groupId, userId, userContactId)
|
||||
|
||||
updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo
|
||||
updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences}
|
||||
updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, image, groupPreferences}
|
||||
| displayName == newName = liftIO $ do
|
||||
currentTs <- getCurrentTime
|
||||
updateGroupProfile_ currentTs $> (g :: GroupInfo) {groupProfile = p', fullGroupPreferences}
|
||||
@@ -3748,14 +3745,14 @@ updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, grou
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_profiles
|
||||
SET display_name = ?, full_name = ?, description = ?, image = ?, preferences = ?, updated_at = ?
|
||||
SET display_name = ?, full_name = ?, image = ?, preferences = ?, updated_at = ?
|
||||
WHERE group_profile_id IN (
|
||||
SELECT group_profile_id
|
||||
FROM groups
|
||||
WHERE user_id = ? AND group_id = ?
|
||||
)
|
||||
|]
|
||||
(newName, fullName, description, image, groupPreferences, currentTs, userId, groupId)
|
||||
(newName, fullName, image, groupPreferences, currentTs, userId, groupId)
|
||||
updateGroup_ ldn currentTs = do
|
||||
DB.execute
|
||||
db
|
||||
|
||||
@@ -724,7 +724,6 @@ fromLocalProfile LocalProfile {displayName, fullName, image, preferences} =
|
||||
data GroupProfile = GroupProfile
|
||||
{ displayName :: GroupName,
|
||||
fullName :: Text,
|
||||
description :: Maybe Text,
|
||||
image :: Maybe ImageData,
|
||||
groupPreferences :: Maybe GroupPreferences
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ responseToView user_ testView ts = \case
|
||||
CRGroupMemberInfo g m cStats -> viewGroupMemberInfo g m cStats
|
||||
CRContactSwitch ct progress -> viewContactSwitch ct progress
|
||||
CRGroupMemberSwitch g m progress -> viewGroupMemberSwitch g m progress
|
||||
CRConnectionVerified verified code -> [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code]
|
||||
CRCodeVerification verified code -> [plain $ if verified then "connection verified" else "error: current connection code is " <> code]
|
||||
CRContactCode ct code -> viewContactCode ct code testView
|
||||
CRGroupMemberCode g m code -> viewGroupMemberCode g m code testView
|
||||
CRNewChatItem (AChatItem _ _ chat item) -> unmuted chat item $ viewChatItem chat item False ts
|
||||
@@ -184,7 +184,6 @@ responseToView user_ testView ts = \case
|
||||
CRGroupRemoved g -> [ttyFullGroup g <> ": you are no longer a member or group deleted"]
|
||||
CRGroupDeleted g m -> [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> groupName' g) <> " to delete the local copy of the group"]
|
||||
CRGroupUpdated g g' m -> viewGroupUpdated g g' m
|
||||
CRGroupProfile g -> viewGroupProfile g
|
||||
CRGroupLinkCreated g cReq -> groupLink_ "Group link is created!" g cReq
|
||||
CRGroupLink g cReq -> groupLink_ "Group link:" g cReq
|
||||
CRGroupLinkDeleted g -> viewGroupLinkDeleted g
|
||||
@@ -489,9 +488,9 @@ viewReceivedContactRequest c Profile {fullName} =
|
||||
]
|
||||
|
||||
viewGroupCreated :: GroupInfo -> [StyledString]
|
||||
viewGroupCreated g@GroupInfo {localDisplayName = n} =
|
||||
viewGroupCreated g@GroupInfo {localDisplayName} =
|
||||
[ "group " <> ttyFullGroup g <> " is created",
|
||||
"to add members use " <> highlight ("/a " <> n <> " <name>") <> " or " <> highlight ("/create link #" <> n)
|
||||
"use " <> highlight ("/a " <> localDisplayName <> " <name>") <> " to add members"
|
||||
]
|
||||
|
||||
viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString]
|
||||
@@ -582,7 +581,7 @@ viewContactConnected ct@Contact {localDisplayName} userIncognitoProfile testView
|
||||
where
|
||||
message =
|
||||
[ ttyFullContact ct <> ": contact is connected, your incognito profile for this contact is " <> incognitoProfile' profile,
|
||||
"use " <> highlight ("/i " <> localDisplayName) <> " to print out this incognito profile again"
|
||||
"use " <> highlight ("/info " <> localDisplayName) <> " to print out this incognito profile again"
|
||||
]
|
||||
Nothing ->
|
||||
[ttyFullContact ct <> ": contact is connected"]
|
||||
@@ -810,8 +809,8 @@ viewCountactUserPref = \case
|
||||
|
||||
viewGroupUpdated :: GroupInfo -> GroupInfo -> Maybe GroupMember -> [StyledString]
|
||||
viewGroupUpdated
|
||||
GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, description, image, groupPreferences = gps}}
|
||||
g'@GroupInfo {localDisplayName = n', groupProfile = GroupProfile {fullName = fullName', description = description', image = image', groupPreferences = gps'}}
|
||||
GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, image, groupPreferences = gps}}
|
||||
g'@GroupInfo {localDisplayName = n', groupProfile = GroupProfile {fullName = fullName', image = image', groupPreferences = gps'}}
|
||||
m = do
|
||||
let update = groupProfileUpdated <> groupPrefsUpdated
|
||||
if null update
|
||||
@@ -819,35 +818,21 @@ viewGroupUpdated
|
||||
else memberUpdated <> update
|
||||
where
|
||||
memberUpdated = maybe [] (\m' -> [ttyMember m' <> " updated group " <> ttyGroup n <> ":"]) m
|
||||
groupProfileUpdated =
|
||||
["changed to " <> ttyFullGroup g' | n /= n']
|
||||
<> ["full name " <> if T.null fullName' || fullName' == n' then "removed" else "changed to: " <> plain fullName' | n == n' && fullName /= fullName']
|
||||
<> ["profile image " <> maybe "removed" (const "updated") image' | image /= image']
|
||||
<> (if description == description' then [] else maybe ["description removed"] ((bold' "description changed to:" :) . map plain . T.lines) description')
|
||||
groupProfileUpdated
|
||||
| n == n' && fullName == fullName' && image == image' = []
|
||||
| n == n' && fullName == fullName' = ["profile image " <> (if isNothing image' then "removed" else "updated")]
|
||||
| n == n' = ["full name " <> if T.null fullName' || fullName' == n' then "removed" else "changed to " <> plain fullName']
|
||||
| otherwise = ["changed to " <> ttyFullGroup g']
|
||||
groupPrefsUpdated
|
||||
| null prefs = []
|
||||
| otherwise = bold' "updated group preferences:" : prefs
|
||||
| otherwise = "updated group preferences:" : prefs
|
||||
where
|
||||
prefs = mapMaybe viewPref allGroupFeatures
|
||||
viewPref pt
|
||||
| pref gps == pref gps' = Nothing
|
||||
| otherwise = Just $ plain (groupFeatureToText pt) <> " enabled: " <> plain (groupPrefToText $ pref gps')
|
||||
where
|
||||
pref = getGroupPreference pt . mergeGroupPreferences
|
||||
|
||||
viewGroupProfile :: GroupInfo -> [StyledString]
|
||||
viewGroupProfile g@GroupInfo {groupProfile = GroupProfile {description, image, groupPreferences = gps}} =
|
||||
[ttyFullGroup g]
|
||||
<> maybe [] (const ["has profile image"]) image
|
||||
<> maybe [] ((bold' "description:" :) . map plain . T.lines) description
|
||||
<> (bold' "group preferences:" : map viewPref allGroupFeatures)
|
||||
where
|
||||
viewPref pt = plain (groupFeatureToText pt) <> " enabled: " <> plain (groupPrefToText $ pref gps)
|
||||
where
|
||||
pref = getGroupPreference pt . mergeGroupPreferences
|
||||
|
||||
bold' :: String -> StyledString
|
||||
bold' = styled Bold
|
||||
pref pss = getGroupPreference pt $ mergeGroupPreferences pss
|
||||
|
||||
viewContactAliasUpdated :: Contact -> [StyledString]
|
||||
viewContactAliasUpdated Contact {localDisplayName = n, profile = LocalProfile {localAlias}}
|
||||
|
||||
@@ -215,7 +215,7 @@ getTermLine :: TestCC -> IO String
|
||||
getTermLine cc =
|
||||
5000000 `timeout` atomically (readTQueue $ termQ cc) >>= \case
|
||||
Just s -> do
|
||||
-- uncomment 2 lines below to echo virtual terminal
|
||||
-- uncomment code below to echo virtual terminal
|
||||
-- name <- userName cc
|
||||
-- putStrLn $ name <> ": " <> s
|
||||
pure s
|
||||
|
||||
@@ -76,7 +76,6 @@ chatTests = do
|
||||
it "update group profile" testUpdateGroupProfile
|
||||
it "update member role" testUpdateMemberRole
|
||||
it "unused contacts are deleted after all their groups are deleted" testGroupDeleteUnusedContacts
|
||||
it "group description is shown as the first message to new members" testGroupDescription
|
||||
describe "async group connections" $ do
|
||||
xit "create and join group when clients go offline" testGroupAsync
|
||||
describe "user profiles" $ do
|
||||
@@ -499,7 +498,7 @@ testGroupShared alice bob cath checkMessages = do
|
||||
connectUsers alice cath
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> "/a team bob"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #team sent to bob",
|
||||
@@ -634,7 +633,7 @@ testGroup2 =
|
||||
connectUsers alice dan
|
||||
alice ##> "/g club"
|
||||
alice <## "group #club is created"
|
||||
alice <## "to add members use /a club <name> or /create link #club"
|
||||
alice <## "use /a club <name> to add members"
|
||||
alice ##> "/a club bob"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #club sent to bob",
|
||||
@@ -852,10 +851,10 @@ testGroupSameName =
|
||||
\alice _ -> do
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team_1 (team) is created"
|
||||
alice <## "to add members use /a team_1 <name> or /create link #team_1"
|
||||
alice <## "use /a team_1 <name> to add members"
|
||||
|
||||
testGroupDeleteWhenInvited :: IO ()
|
||||
testGroupDeleteWhenInvited =
|
||||
@@ -864,7 +863,7 @@ testGroupDeleteWhenInvited =
|
||||
connectUsers alice bob
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> "/a team bob"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #team sent to bob",
|
||||
@@ -891,7 +890,7 @@ testGroupReAddInvited =
|
||||
connectUsers alice bob
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> "/a team bob"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #team sent to bob",
|
||||
@@ -926,7 +925,7 @@ testGroupReAddInvitedChangeRole =
|
||||
connectUsers alice bob
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> "/a team bob"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #team sent to bob",
|
||||
@@ -966,7 +965,7 @@ testGroupDeleteInvitedContact =
|
||||
connectUsers alice bob
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> "/a team bob"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #team sent to bob",
|
||||
@@ -997,7 +996,7 @@ testDeleteGroupMemberProfileKept =
|
||||
-- group 1
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> "/a team bob"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #team sent to bob",
|
||||
@@ -1016,7 +1015,7 @@ testDeleteGroupMemberProfileKept =
|
||||
-- group 2
|
||||
alice ##> "/g club"
|
||||
alice <## "group #club is created"
|
||||
alice <## "to add members use /a club <name> or /create link #club"
|
||||
alice <## "use /a club <name> to add members"
|
||||
alice ##> "/a club bob"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #club sent to bob",
|
||||
@@ -1109,7 +1108,7 @@ testGroupList =
|
||||
createGroup2 "team" alice bob
|
||||
alice ##> "/g tennis"
|
||||
alice <## "group #tennis is created"
|
||||
alice <## "to add members use /a tennis <name> or /create link #tennis"
|
||||
alice <## "use /a tennis <name> to add members"
|
||||
alice ##> "/a tennis bob"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #tennis sent to bob",
|
||||
@@ -1378,7 +1377,7 @@ testUpdateMemberRole =
|
||||
connectUsers alice bob
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
addMember "team" alice bob GRAdmin
|
||||
alice ##> "/mr team bob member"
|
||||
alice <## "#team: you changed the role of bob from admin to member"
|
||||
@@ -1426,7 +1425,7 @@ testGroupDeleteUnusedContacts =
|
||||
-- create group 2
|
||||
alice ##> "/g club"
|
||||
alice <## "group #club is created"
|
||||
alice <## "to add members use /a club <name> or /create link #club"
|
||||
alice <## "use /a club <name> to add members"
|
||||
alice ##> "/a club bob"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #club sent to bob",
|
||||
@@ -1501,70 +1500,6 @@ testGroupDeleteUnusedContacts =
|
||||
cath ##> ("/d #" <> group)
|
||||
cath <## ("#" <> group <> ": you deleted the group")
|
||||
|
||||
testGroupDescription :: IO ()
|
||||
testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do
|
||||
connectUsers alice bob
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
addMember "team" alice bob GRAdmin
|
||||
bob ##> "/j team"
|
||||
concurrentlyN_
|
||||
[ alice <## "#team: bob joined the group",
|
||||
bob <## "#team: you joined the group"
|
||||
]
|
||||
alice ##> "/group_profile team"
|
||||
alice <## "#team"
|
||||
groupInfo alice
|
||||
alice ##> "/group_descr team Welcome to the team!"
|
||||
alice <## "description changed to:"
|
||||
alice <## "Welcome to the team!"
|
||||
bob <## "alice updated group #team:"
|
||||
bob <## "description changed to:"
|
||||
bob <## "Welcome to the team!"
|
||||
alice ##> "/group_profile team"
|
||||
alice <## "#team"
|
||||
alice <## "description:"
|
||||
alice <## "Welcome to the team!"
|
||||
groupInfo alice
|
||||
connectUsers alice cath
|
||||
addMember "team" alice cath GRMember
|
||||
cath ##> "/j team"
|
||||
concurrentlyN_
|
||||
[ alice <## "#team: cath joined the group",
|
||||
do
|
||||
cath <## "#team: you joined the group"
|
||||
cath <# "#team alice> Welcome to the team!"
|
||||
cath <## "#team: member bob (Bob) is connected",
|
||||
do
|
||||
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
|
||||
bob <## "#team: new member cath is connected"
|
||||
]
|
||||
connectUsers bob dan
|
||||
addMember "team" bob dan GRMember
|
||||
dan ##> "/j team"
|
||||
concurrentlyN_
|
||||
[ bob <## "#team: dan joined the group",
|
||||
do
|
||||
dan <## "#team: you joined the group"
|
||||
dan <# "#team bob> Welcome to the team!"
|
||||
dan
|
||||
<### [ "#team: member alice (Alice) is connected",
|
||||
"#team: member cath (Catherine) is connected"
|
||||
],
|
||||
bobAddedDan alice,
|
||||
bobAddedDan cath
|
||||
]
|
||||
where
|
||||
groupInfo alice = do
|
||||
alice <## "group preferences:"
|
||||
alice <## "Direct messages enabled: on"
|
||||
alice <## "Full deletion enabled: off"
|
||||
alice <## "Voice messages enabled: on"
|
||||
bobAddedDan cc = do
|
||||
cc <## "#team: bob added dan (Daniel) to the group (connecting...)"
|
||||
cc <## "#team: new member dan is connected"
|
||||
|
||||
testGroupAsync :: IO ()
|
||||
testGroupAsync = withTmpFiles $ do
|
||||
print (0 :: Integer)
|
||||
@@ -1573,7 +1508,7 @@ testGroupAsync = withTmpFiles $ do
|
||||
connectUsers alice bob
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> "/a team bob"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #team sent to bob",
|
||||
@@ -2756,7 +2691,7 @@ testAutoReplyMessageInIncognito = testChat2 aliceProfile bobProfile $
|
||||
do
|
||||
alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
|
||||
alice
|
||||
<### [ "use /i bob to print out this incognito profile again",
|
||||
<### [ "use /info bob to print out this incognito profile again",
|
||||
WithTime "i @bob hello!"
|
||||
]
|
||||
]
|
||||
@@ -2775,10 +2710,10 @@ testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfi
|
||||
concurrentlyN_
|
||||
[ do
|
||||
bob <## (aliceIncognito <> ": contact is connected, your incognito profile for this contact is " <> bobIncognito)
|
||||
bob <## ("use /i " <> aliceIncognito <> " to print out this incognito profile again"),
|
||||
bob <## ("use /info " <> aliceIncognito <> " to print out this incognito profile again"),
|
||||
do
|
||||
alice <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> aliceIncognito)
|
||||
alice <## ("use /i " <> bobIncognito <> " to print out this incognito profile again")
|
||||
alice <## ("use /info " <> bobIncognito <> " to print out this incognito profile again")
|
||||
]
|
||||
-- after turning incognito mode off conversation is incognito
|
||||
alice #$> ("/incognito off", id, "ok")
|
||||
@@ -2857,7 +2792,7 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $
|
||||
concurrentlyN_
|
||||
[ do
|
||||
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
|
||||
bob <## "use /i alice to print out this incognito profile again",
|
||||
bob <## "use /info alice to print out this incognito profile again",
|
||||
alice <## (bobIncognito <> ": contact is connected")
|
||||
]
|
||||
-- after turning incognito mode off conversation is incognito
|
||||
@@ -2893,7 +2828,7 @@ testAcceptContactRequestIncognito = testChat2 aliceProfile bobProfile $
|
||||
[ bob <## (aliceIncognito <> ": contact is connected"),
|
||||
do
|
||||
alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
|
||||
alice <## "use /i bob to print out this incognito profile again"
|
||||
alice <## "use /info bob to print out this incognito profile again"
|
||||
]
|
||||
-- after turning incognito mode off conversation is incognito
|
||||
alice #$> ("/incognito off", id, "ok")
|
||||
@@ -2932,13 +2867,13 @@ testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfil
|
||||
concurrentlyN_
|
||||
[ do
|
||||
cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito)
|
||||
cath <## "use /i alice to print out this incognito profile again",
|
||||
cath <## "use /info alice to print out this incognito profile again",
|
||||
alice <## (cathIncognito <> ": contact is connected")
|
||||
]
|
||||
-- alice creates group
|
||||
alice ##> "/g secret_club"
|
||||
alice <## "group #secret_club is created"
|
||||
alice <## "to add members use /a secret_club <name> or /create link #secret_club"
|
||||
alice <## "use /a secret_club <name> to add members"
|
||||
-- alice invites bob
|
||||
alice ##> "/a secret_club bob"
|
||||
concurrentlyN_
|
||||
@@ -3118,13 +3053,13 @@ testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $
|
||||
[ bob <## (aliceIncognito <> ": contact is connected"),
|
||||
do
|
||||
alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
|
||||
alice <## "use /i bob to print out this incognito profile again"
|
||||
alice <## "use /info bob to print out this incognito profile again"
|
||||
]
|
||||
-- alice creates group non incognito
|
||||
alice #$> ("/incognito off", id, "ok")
|
||||
alice ##> "/g club"
|
||||
alice <## "group #club is created"
|
||||
alice <## "to add members use /a club <name> or /create link #club"
|
||||
alice <## "use /a club <name> to add members"
|
||||
alice ##> "/a club bob"
|
||||
alice <## "you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito"
|
||||
-- bob doesn't receive invitation
|
||||
@@ -3148,7 +3083,7 @@ testCantSeeGlobalPrefsUpdateIncognito = testChat3 aliceProfile bobProfile cathPr
|
||||
[ bob <## (aliceIncognito <> ": contact is connected"),
|
||||
do
|
||||
alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
|
||||
alice <## "use /i bob to print out this incognito profile again",
|
||||
alice <## "use /info bob to print out this incognito profile again",
|
||||
do
|
||||
cath <## "alice (Alice): contact is connected"
|
||||
]
|
||||
@@ -3197,12 +3132,12 @@ testDeleteContactThenGroupDeletesIncognitoProfile = testChat2 aliceProfile bobPr
|
||||
[ alice <## (bobIncognito <> ": contact is connected"),
|
||||
do
|
||||
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
|
||||
bob <## "use /i alice to print out this incognito profile again"
|
||||
bob <## "use /info alice to print out this incognito profile again"
|
||||
]
|
||||
-- bob joins group using incognito profile
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> ("/a team " <> bobIncognito)
|
||||
concurrentlyN_
|
||||
[ alice <## ("invitation to join the group #team sent to " <> bobIncognito),
|
||||
@@ -3249,12 +3184,12 @@ testDeleteGroupThenContactDeletesIncognitoProfile = testChat2 aliceProfile bobPr
|
||||
[ alice <## (bobIncognito <> ": contact is connected"),
|
||||
do
|
||||
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
|
||||
bob <## "use /i alice to print out this incognito profile again"
|
||||
bob <## "use /info alice to print out this incognito profile again"
|
||||
]
|
||||
-- bob joins group using incognito profile
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> ("/a team " <> bobIncognito)
|
||||
concurrentlyN_
|
||||
[ alice <## ("invitation to join the group #team sent to " <> bobIncognito),
|
||||
@@ -3514,19 +3449,19 @@ testProhibitDirectMessages =
|
||||
addMember "team" cath dan GRMember
|
||||
dan ##> "/j #team"
|
||||
concurrentlyN_
|
||||
[ cath <## "#team: dan joined the group",
|
||||
[ cath <## ("#team: dan joined the group"),
|
||||
do
|
||||
dan <## "#team: you joined the group"
|
||||
dan <## ("#team: you joined the group")
|
||||
dan
|
||||
<### [ "#team: member alice (Alice) is connected",
|
||||
"#team: member bob (Bob) is connected"
|
||||
],
|
||||
do
|
||||
alice <## "#team: cath added dan (Daniel) to the group (connecting...)"
|
||||
alice <## "#team: new member dan is connected",
|
||||
alice <## ("#team: cath added dan (Daniel) to the group (connecting...)")
|
||||
alice <## ("#team: new member dan is connected"),
|
||||
do
|
||||
bob <## "#team: cath added dan (Daniel) to the group (connecting...)"
|
||||
bob <## "#team: new member dan is connected"
|
||||
bob <## ("#team: cath added dan (Daniel) to the group (connecting...)")
|
||||
bob <## ("#team: new member dan is connected")
|
||||
]
|
||||
alice ##> "@dan hi"
|
||||
alice <## "direct messages to indirect contact dan are prohibited"
|
||||
@@ -3573,7 +3508,7 @@ testTestSMPServerConnection =
|
||||
alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:5001"
|
||||
alice <## "SMP server test passed"
|
||||
alice ##> "/smp test smp://LcJU@localhost:5001"
|
||||
alice <## "SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:5001 NETWORK"
|
||||
alice <## ("SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:5001 NETWORK")
|
||||
alice <## "Possibly, certificate fingerprint in server address is incorrect"
|
||||
|
||||
testAsyncInitiatingOffline :: IO ()
|
||||
@@ -4166,7 +4101,7 @@ testGroupLink =
|
||||
\alice bob cath -> do
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> "/show link #team"
|
||||
alice <## "no group link, to create: /create link #team"
|
||||
alice ##> "/create link #team"
|
||||
@@ -4266,7 +4201,7 @@ testGroupLinkDeleteGroupRejoin =
|
||||
\alice bob -> do
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> "/create link #team"
|
||||
gLink <- getGroupLink alice "team" True
|
||||
bob ##> ("/c " <> gLink)
|
||||
@@ -4322,7 +4257,7 @@ testGroupLinkContactUsed =
|
||||
\alice bob -> do
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> "/create link #team"
|
||||
gLink <- getGroupLink alice "team" True
|
||||
bob ##> ("/c " <> gLink)
|
||||
@@ -4361,14 +4296,14 @@ testGroupLinkIncognitoMembership =
|
||||
concurrentlyN_
|
||||
[ do
|
||||
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
|
||||
bob <## "use /i alice to print out this incognito profile again",
|
||||
bob <## "use /info alice to print out this incognito profile again",
|
||||
alice <## (bobIncognito <> ": contact is connected")
|
||||
]
|
||||
bob #$> ("/incognito off", id, "ok")
|
||||
-- alice creates group
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
-- alice invites bob
|
||||
alice ##> ("/a team " <> bobIncognito)
|
||||
concurrentlyN_
|
||||
@@ -4391,7 +4326,7 @@ testGroupLinkIncognitoMembership =
|
||||
concurrentlyN_
|
||||
[ do
|
||||
bob <## ("cath (Catherine): contact is connected, your incognito profile for this contact is " <> bobIncognito)
|
||||
bob <## "use /i cath to print out this incognito profile again"
|
||||
bob <## "use /info cath to print out this incognito profile again"
|
||||
bob <## "cath invited to group #team via your group link"
|
||||
bob <## "#team: cath joined the group",
|
||||
do
|
||||
@@ -4417,12 +4352,12 @@ testGroupLinkIncognitoMembership =
|
||||
concurrentlyN_
|
||||
[ do
|
||||
bob <## (danIncognito <> ": contact is connected, your incognito profile for this contact is " <> bobIncognito)
|
||||
bob <## ("use /i " <> danIncognito <> " to print out this incognito profile again")
|
||||
bob <## ("use /info " <> danIncognito <> " to print out this incognito profile again")
|
||||
bob <## (danIncognito <> " invited to group #team via your group link")
|
||||
bob <## ("#team: " <> danIncognito <> " joined the group"),
|
||||
do
|
||||
dan <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> danIncognito)
|
||||
dan <## ("use /i " <> bobIncognito <> " to print out this incognito profile again")
|
||||
dan <## ("use /info " <> bobIncognito <> " to print out this incognito profile again")
|
||||
dan <## ("#team: you joined the group incognito as " <> danIncognito)
|
||||
dan
|
||||
<### [ "#team: member alice (Alice) is connected",
|
||||
@@ -4472,7 +4407,7 @@ testGroupLinkUnusedHostContactDeleted =
|
||||
-- create group 1
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> or /create link #team"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> "/create link #team"
|
||||
gLinkTeam <- getGroupLink alice "team" True
|
||||
bob ##> ("/c " <> gLinkTeam)
|
||||
@@ -4490,7 +4425,7 @@ testGroupLinkUnusedHostContactDeleted =
|
||||
-- create group 2
|
||||
alice ##> "/g club"
|
||||
alice <## "group #club is created"
|
||||
alice <## "to add members use /a club <name> or /create link #club"
|
||||
alice <## "use /a club <name> to add members"
|
||||
alice ##> "/create link #club"
|
||||
gLinkClub <- getGroupLink alice "club" True
|
||||
bob ##> ("/c " <> gLinkClub)
|
||||
@@ -4563,7 +4498,7 @@ testGroupLinkIncognitoUnusedHostContactsDeleted =
|
||||
createGroupBobIncognito alice bob group bobsAliceContact = do
|
||||
alice ##> ("/g " <> group)
|
||||
alice <## ("group #" <> group <> " is created")
|
||||
alice <## ("to add members use /a " <> group <> " <name> or /create link #" <> group)
|
||||
alice <## ("use /a " <> group <> " <name> to add members")
|
||||
alice ##> ("/create link #" <> group)
|
||||
gLinkTeam <- getGroupLink alice group True
|
||||
bob ##> ("/c " <> gLinkTeam)
|
||||
@@ -4578,7 +4513,7 @@ testGroupLinkIncognitoUnusedHostContactsDeleted =
|
||||
alice <## ("#" <> group <> ": " <> bobIncognito <> " joined the group"),
|
||||
do
|
||||
bob <## (bobsAliceContact <> " (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
|
||||
bob <## ("use /i " <> bobsAliceContact <> " to print out this incognito profile again")
|
||||
bob <## ("use /info " <> bobsAliceContact <> " to print out this incognito profile again")
|
||||
bob <## ("#" <> group <> ": you joined the group incognito as " <> bobIncognito)
|
||||
]
|
||||
pure bobIncognito
|
||||
@@ -4637,17 +4572,12 @@ testMarkContactVerified =
|
||||
aCode <- getTermLine bob
|
||||
bCode `shouldBe` aCode
|
||||
alice ##> "/verify bob 123"
|
||||
alice <##. "connection not verified, current code is "
|
||||
alice <##. "error: current connection code is "
|
||||
alice ##> ("/verify bob " <> aCode)
|
||||
alice <## "connection verified"
|
||||
alice ##> "/i bob"
|
||||
bobInfo alice
|
||||
alice <## "connection verified"
|
||||
alice ##> "/verify bob"
|
||||
alice <##. "connection not verified, current code is "
|
||||
alice ##> "/i bob"
|
||||
bobInfo alice
|
||||
alice <## "connection not verified, use /code command to see security code"
|
||||
where
|
||||
bobInfo alice = do
|
||||
alice <## "contact ID: 2"
|
||||
@@ -4668,17 +4598,12 @@ testMarkGroupMemberVerified =
|
||||
aCode <- getTermLine bob
|
||||
bCode `shouldBe` aCode
|
||||
alice ##> "/verify #team bob 123"
|
||||
alice <##. "connection not verified, current code is "
|
||||
alice <##. "error: current connection code is "
|
||||
alice ##> ("/verify #team bob " <> aCode)
|
||||
alice <## "connection verified"
|
||||
alice ##> "/i #team bob"
|
||||
bobInfo alice
|
||||
alice <## "connection verified"
|
||||
alice ##> "/verify #team bob"
|
||||
alice <##. "connection not verified, current code is "
|
||||
alice ##> "/i #team bob"
|
||||
bobInfo alice
|
||||
alice <## "connection not verified, use /code command to see security code"
|
||||
where
|
||||
bobInfo alice = do
|
||||
alice <## "group ID: 1"
|
||||
@@ -4776,7 +4701,7 @@ createGroup2 gName cc1 cc2 = do
|
||||
name2 <- userName cc2
|
||||
cc1 ##> ("/g " <> gName)
|
||||
cc1 <## ("group #" <> gName <> " is created")
|
||||
cc1 <## ("to add members use /a " <> gName <> " <name> or /create link #" <> gName)
|
||||
cc1 <## ("use /a " <> gName <> " <name> to add members")
|
||||
addMember gName cc1 cc2 GRAdmin
|
||||
cc2 ##> ("/j " <> gName)
|
||||
concurrently_
|
||||
|
||||
@@ -89,7 +89,7 @@ testProfile :: Profile
|
||||
testProfile = Profile {displayName = "alice", fullName = "Alice", image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), preferences = testChatPreferences}
|
||||
|
||||
testGroupProfile :: GroupProfile
|
||||
testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, image = Nothing, groupPreferences = testGroupPreferences}
|
||||
testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", image = Nothing, groupPreferences = testGroupPreferences}
|
||||
|
||||
decodeChatMessageTest :: Spec
|
||||
decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# .well-known
|
||||
|
||||
This website files allow opening SimpleX Chat links (1-time invitations, contact addresses and groups) directly in the app.
|
||||
|
||||
## Android
|
||||
|
||||
File `assetlinks.json` includes certificate hashes for:
|
||||
|
||||
- Play Store (5E:3E:DC:C2:00:FB:A8:D5:F4:88:F3:CA:4C:32:5B:05:78:C5:6A:9C:03:A1:CC:B5:92:9C:D7:5C:7E:57:E2:4D)
|
||||
- APK in GitHub releases (3C:52:C4:FD:3C:AD:1C:07:C9:B0:0A:70:80:E3:58:FA:B9:FE:FC:B8:AF:5A:EC:14:77:65:F1:6D:0F:21:AD:85)
|
||||
- F-Droid (AE:C1:95:DC:FD:46:14:BD:3A:91:EC:26:D1:D5:14:C8:75:71:C5:CC:8D:CF:48:08:3F:92:83:14:3C:A2:B9:A6)
|
||||
|
||||
## iOS
|
||||
|
||||
`apple-app-site-association` currently does not work, as it needs to be served with `Content-type: application/json; charset=utf-8` and GitHub pages do not support adding this header to files without JSON extension.
|
||||
@@ -8,8 +8,7 @@
|
||||
"package_name": "chat.simplex.app",
|
||||
"sha256_cert_fingerprints": [
|
||||
"5E:3E:DC:C2:00:FB:A8:D5:F4:88:F3:CA:4C:32:5B:05:78:C5:6A:9C:03:A1:CC:B5:92:9C:D7:5C:7E:57:E2:4D",
|
||||
"3C:52:C4:FD:3C:AD:1C:07:C9:B0:0A:70:80:E3:58:FA:B9:FE:FC:B8:AF:5A:EC:14:77:65:F1:6D:0F:21:AD:85",
|
||||
"AE:C1:95:DC:FD:46:14:BD:3A:91:EC:26:D1:D5:14:C8:75:71:C5:CC:8D:CF:48:08:3F:92:83:14:3C:A2:B9:A6"
|
||||
"3C:52:C4:FD:3C:AD:1C:07:C9:B0:0A:70:80:E3:58:FA:B9:FE:FC:B8:AF:5A:EC:14:77:65:F1:6D:0F:21:AD:85"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user