diff --git a/README.md b/README.md index 6cc3b5bb77..922517a4e4 100644 --- a/README.md +++ b/README.md @@ -148,12 +148,12 @@ We plan to add soon: ## For developers -We plan that the SimpleX platform will grow into the platform supporting any distributed Internet application. This will allow you to build any service that people can access via chat, with custom web-based UI widgets that anybody with basic HTML/CSS/JavaScript knowledge can create in a few hours. +You can: -You already can: - -- use SimpleX Chat library to integrate chat functionality into your apps. -- use SimpleX Chat bot templates in Haskell to build your own chat bot services (TypeScript SDK is coming soon). +- use SimpleX Chat library to integrate chat functionality into your mobile apps. +- create chat bots and services in Haskell - see [simple](./apps/simplex-bot/) and more [advanced chat bot example](./apps/simplex-bot-advanced/). +- create chat bots and services in any language running SimpleX Chat terminal CLI as a local WebSocket server. See [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) and [JavaScipt chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js). +- run [simplex-chat terminal CLI](./docs/CLI.md) to execute individual chat commands, e.g. to send messages as part of shell script execution. If you are considering developing with SimpleX platform please get in touch for any advice and support. diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index eba863a3bf..1d95cfeddd 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -119,6 +119,10 @@ dependencies { // Biometric authentication implementation 'androidx.biometric:biometric:1.2.0-alpha04' + // GIFs support + implementation "io.coil-kt:coil-compose:2.1.0" + implementation "io.coil-kt:coil-gif:2.1.0" + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 3eec9ba191..10321ac912 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -777,7 +777,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager val cItem = r.chatItem.chatItem chatModel.addChatItem(cInfo, cItem) val file = cItem.file - if (cItem.content.msgContent is MsgContent.MCImage && file != null && file.fileSize <= MAX_IMAGE_SIZE && appPrefs.privacyAcceptImages.get()) { + if (cItem.content.msgContent is MsgContent.MCImage && file != null && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV && appPrefs.privacyAcceptImages.get()) { withApi { receiveFile(file.fileId) } } if (!cItem.chatDir.sent && !cItem.isCall && (!isAppOnForeground(appContext) || chatModel.chatId.value != cInfo.id)) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt index d96b8a75ca..b3c5b73353 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt @@ -8,6 +8,7 @@ import android.content.* import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.ImageDecoder +import android.graphics.drawable.AnimatedImageDrawable import android.net.Uri import android.provider.MediaStore import android.util.Log @@ -146,6 +147,7 @@ fun ComposeView( val textStyle = remember { mutableStateOf(smallFont) } // attachments val chosenImage = remember { mutableStateOf(null) } + val chosenAnimImage = remember { mutableStateOf(null) } val chosenFile = remember { mutableStateOf(null) } val photoUri = remember { mutableStateOf(null) } val photoTmpFile = remember { mutableStateOf(null) } @@ -194,24 +196,23 @@ fun ComposeView( Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show() } } - val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery()) { uri: Uri? -> + val processPickedImage = { uri: Uri? -> if (uri != null) { val source = ImageDecoder.createSource(context.contentResolver, uri) + val drawable = ImageDecoder.decodeDrawable(source) val bitmap = ImageDecoder.decodeBitmap(source) - chosenImage.value = bitmap - val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000) - composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(imagePreview)) - } - } - val galleryLauncherFallback = rememberGetContentLauncher { uri: Uri? -> - if (uri != null) { - val source = ImageDecoder.createSource(context.contentResolver, uri) - val bitmap = ImageDecoder.decodeBitmap(source) - chosenImage.value = bitmap + if (drawable is AnimatedImageDrawable) { + // It's a gif or webp + chosenAnimImage.value = uri + } else { + chosenImage.value = bitmap + } val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000) composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(imagePreview)) } } + val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery(), processPickedImage) + val galleryLauncherFallback = rememberGetContentLauncher(processPickedImage) val filesLauncher = rememberGetContentLauncher { uri: Uri? -> if (uri != null) { val fileSize = getFileSize(context, uri) @@ -334,6 +335,7 @@ fun ComposeView( composeState.value = ComposeState(useLinkPreviews = useLinkPreviews) textStyle.value = smallFont chosenImage.value = null + chosenAnimImage.value = null chosenFile.value = null linkUrl.value = null prevLinkUrl.value = null @@ -376,6 +378,13 @@ fun ComposeView( mc = MsgContent.MCImage(cs.message, preview.image) } } + val chosenGifImageVal = chosenAnimImage.value + if (chosenGifImageVal != null) { + file = saveAnimImage(context, chosenGifImageVal) + if (file != null) { + mc = MsgContent.MCImage(cs.message, preview.image) + } + } } is ComposePreview.FilePreview -> { val chosenFileVal = chosenFile.value @@ -436,6 +445,7 @@ fun ComposeView( fun cancelImage() { composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) chosenImage.value = null + chosenAnimImage.value = null } fun cancelFile() { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt index 139e8e31a9..4e5da7a85f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt @@ -1,4 +1,5 @@ import android.graphics.Bitmap +import android.os.Build.VERSION.SDK_INT import androidx.compose.foundation.Image import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* @@ -13,14 +14,24 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.core.content.FileProvider +import chat.simplex.app.BuildConfig import chat.simplex.app.R import chat.simplex.app.model.CIFile import chat.simplex.app.model.CIFileStatus import chat.simplex.app.views.helpers.* +import coil.ImageLoader +import coil.compose.rememberAsyncImagePainter +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import coil.request.ImageRequest +import java.io.File @Composable fun CIImageView( @@ -88,13 +99,46 @@ fun CIImageView( ) } + @Composable + fun imageView(painter: Painter, onClick: () -> Unit) { + Image( + painter, + contentDescription = stringResource(R.string.image_descr), + // .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView + // if text is short and take all available width if text is long + modifier = Modifier + .width(1000.dp) + .combinedClickable( + onLongClick = { showMenu.value = true }, + onClick = onClick + ), + contentScale = ContentScale.FillWidth, + ) + } + Box(contentAlignment = Alignment.TopEnd) { val context = LocalContext.current val imageBitmap: Bitmap? = getLoadedImage(context, file) - if (imageBitmap != null) { - imageView(imageBitmap, onClick = { + val filePath = getLoadedFilePath(context, file) + if (imageBitmap != null && filePath != null) { + val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) + val imageLoader = ImageLoader.Builder(context) + .components { + if (SDK_INT >= 28) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + } + .build() + val imagePainter = rememberAsyncImagePainter( + ImageRequest.Builder(context).data(data = uri).size(coil.size.Size.ORIGINAL).build(), + placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil + imageLoader = imageLoader + ) + imageView(imagePainter, onClick = { if (getLoadedFilePath(context, file) != null) { - ModalManager.shared.showCustomModal { close -> ImageFullScreenView(imageBitmap, close) } + ModalManager.shared.showCustomModal { close -> ImageFullScreenView(imageBitmap, uri, close) } } }) } else { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ImageFullScreenView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ImageFullScreenView.kt index 46051fed52..13608278b9 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ImageFullScreenView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ImageFullScreenView.kt @@ -1,4 +1,6 @@ import android.graphics.Bitmap +import android.net.Uri +import android.os.Build import androidx.activity.compose.BackHandler import androidx.compose.foundation.* import androidx.compose.foundation.gestures.detectTransformGestures @@ -7,13 +9,21 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import chat.simplex.app.R +import coil.ImageLoader +import coil.compose.rememberAsyncImagePainter +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import coil.request.ImageRequest +import coil.size.Size @Composable -fun ImageFullScreenView(imageBitmap: Bitmap, close: () -> Unit) { +fun ImageFullScreenView(imageBitmap: Bitmap, uri: Uri, close: () -> Unit) { BackHandler(onBack = close) Column( Modifier @@ -24,8 +34,23 @@ fun ImageFullScreenView(imageBitmap: Bitmap, close: () -> Unit) { var scale by remember { mutableStateOf(1f) } var translationX by remember { mutableStateOf(0f) } var translationY by remember { mutableStateOf(0f) } + // I'm making a new instance of imageLoader here because if I use one instance in multiple places + // after end of composition here a GIF from the first instance will be paused automatically which isn't what I want + val imageLoader = ImageLoader.Builder(LocalContext.current) + .components { + if (Build.VERSION.SDK_INT >= 28) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + } + .build() Image( - imageBitmap.asImageBitmap(), + rememberAsyncImagePainter( + ImageRequest.Builder(LocalContext.current).data(data = uri).size(Size.ORIGINAL).build(), + placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil + imageLoader = imageLoader + ), contentDescription = stringResource(R.string.image_descr), contentScale = ContentScale.Fit, modifier = Modifier diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt index 1045605cd1..2b46110d58 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt @@ -57,8 +57,17 @@ fun saveImage(cxt: Context, ciFile: CIFile?) { val fileName = ciFile?.fileName if (filePath != null && fileName != null) { val values = ContentValues() + val lowercaseName = fileName.lowercase() + val mimeType = when { + lowercaseName.endsWith(".png") -> "image/png" + lowercaseName.endsWith(".gif") -> "image/gif" + lowercaseName.endsWith(".webp") -> "image/webp" + lowercaseName.endsWith(".avif") -> "image/avif" + lowercaseName.endsWith(".svg") -> "image/svg+xml" + else -> "image/jpeg" + } values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) - values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") + values.put(MediaStore.Images.Media.MIME_TYPE, mimeType) values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) values.put(MediaStore.MediaColumns.TITLE, fileName) val uri = cxt.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index 1e635aba66..d09fcc4ce1 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -212,6 +212,7 @@ private fun spannableStringToAnnotatedString( // maximum image file size to be auto-accepted const val MAX_IMAGE_SIZE: Long = 236700 +const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2 const val MAX_FILE_SIZE: Long = 8000000 fun getFilesDirectory(context: Context): String { @@ -320,6 +321,32 @@ fun saveImage(context: Context, image: Bitmap): String? { } } +fun saveAnimImage(context: Context, uri: Uri): String? { + return try { + val filename = getFileName(context, uri)?.lowercase() + var ext = when { + // remove everything but extension + filename?.contains(".") == true -> filename.replaceBeforeLast('.', "").replace(".", "") + else -> "gif" + } + // Just in case the image has a strange extension + if (ext.length < 3 || ext.length > 4) ext = "gif" + val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) + val fileToSave = uniqueCombine(context, "IMG_${timestamp}.$ext") + val file = File(getAppFilePath(context, fileToSave)) + val output = FileOutputStream(file) + context.contentResolver.openInputStream(uri)!!.use { input -> + output.use { output -> + input.copyTo(output) + } + } + fileToSave + } catch (e: Exception) { + Log.e(chat.simplex.app.TAG, "Util.kt saveAnimImage error: ${e.message}") + null + } +} + fun saveFileFromUri(context: Context, uri: Uri): String? { return try { val inputStream = context.contentResolver.openInputStream(uri) diff --git a/docs/rfcs/2022-09-06-send-small-files.md b/docs/rfcs/2022-09-06-send-small-files.md new file mode 100644 index 0000000000..221f851b6f --- /dev/null +++ b/docs/rfcs/2022-09-06-send-small-files.md @@ -0,0 +1,55 @@ +# Sending small files + +## Problem + +Sending files has a substantial constant overhead, and requires multiple online presenses from both sides, with additional connection handshake. For large files this overhead is justified, as otherwise files would consume queue quota, but for small files it makes sending files slow. + +## Solution + +Send small files in the same connection. There can be two modes of sending files - the one that requires explicit acceptance, but after the acceptance the file will be delivered inline, without creating a new conneciton. Another, when the file will be sent straight after the message - that would require preliminary agreement, per contact - this mode can be useful for small voice messages and gifs. + +## Design + +1. Add optional `fileInline :: Maybe FileInlineMode` property to `FileInvitation`: + +```haskell +data FileInlineMode + = FIInvitation -- recepient must accept + | FIChunks -- file is sent after the message without acceptance +``` + +2. Add `XFileAcptInline SharedMsgId String` message to accept inline files, this can only be sent in case inline mode is offered, so the sender would support it: + +``` +{ + "properties": { + "type": {"enum": ["x.file.acpt.inline"]}, + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "msgId": {"ref": "base64url"}, + "fileName": {"type": "string"} + } + } + } +} +``` + +3. Add `XFileChunks` message that have to be sent in front of the sequence of chunks (sent only in `FIInvitation` mode): + +``` +{ + "properties": { + "type": {"enum": ["x.file.chunks"]}, + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "msgId": {"ref": "base64url"}, + "fileName": {"type": "string"} + } + } + } +} +``` + +4. Support file chunks in the main connection if the previous message was `XFileChunks` or `FileInvitation` in `FIChunks` mode. diff --git a/packages/simplex-chat-client/typescript/README.md b/packages/simplex-chat-client/typescript/README.md index 2aacf3ba01..2228d51648 100644 --- a/packages/simplex-chat-client/typescript/README.md +++ b/packages/simplex-chat-client/typescript/README.md @@ -1,23 +1,43 @@ # SimpleX Chat JavaScript client +This is a TypeScript library that defines WebSocket API client for [SimpleX Chat terminal CLI](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/CLI.md) that should be run as a WebSockets server on any port: + +```bash +simplex-chat -p 5225 +``` + +Client API provides types and functions to: + +- create and change user profile (although, in most cases you can do it manually, via SimpleX Chat terminal app). +- create and accept invitations or connect with the contacts. +- create and manage long-term user address, accepting connection requests automatically. +- create, join and manage group. +- send and receive files. + +## Use cases + +- chat bots: you can implement any logic of connecting with and communicating with SimpleX Chat users. Using chat groups a chat bot can connect SimleX Chat users with each other. +- control of the equipment: e.g. servers or home automation. SimpleX Chat provides secure and authorised connections, so this is more secure than using rest APIs. + +Please share your use cases and implementations. + ## Quick start ``` npm i simplex-chat +npm run build ``` -See example of chat bot in [squaring-bot.js](./examples/squaring-bot.js) +See the example of a simple chat bot in [squaring-bot.js](./examples/squaring-bot.js): + +- start `simplex-chat` as a server on port 5225: `simplex-chat -p 5225 -d test_db` +- run chatbot: `node examples/squaring-bot` +- connect to chatbot via SimpleX Chat client using the address of the chat bot ## Documentation -Please refer to: +Please refer to the available client API in [client.ts](./src/client.ts). -- available client API - [client.ts](./src/client.ts). -- available commands - `ChatCommand` type in [command.ts](./src/command.ts) - if some command is not created as a ChatClient method, you can pass any command object to `sendChatCommand` method, or if the type for some command is not available you can pass command string (same strings as supported in terminal/mobile API) to `sendChatCmdStr` method. -- available chat messages - `ChatResponse` type in [response.ts](./src/command.ts). - -**Please note**: you should NOT use local display names that are supported in terminal app, as they can change when contact profile is updated and you can have race conditions - use commands that use chat IDs. - -## Lisense +## License [AGPL v3](./LICENSE) diff --git a/packages/simplex-chat-client/typescript/examples/squaring-bot.js b/packages/simplex-chat-client/typescript/examples/squaring-bot.js index 79b6dae709..8f874b2f46 100644 --- a/packages/simplex-chat-client/typescript/examples/squaring-bot.js +++ b/packages/simplex-chat-client/typescript/examples/squaring-bot.js @@ -6,15 +6,18 @@ run() async function run() { const chat = await ChatClient.create("ws://localhost:5225") + // this example assumes that you have initialized user profile for chat bot via terminal CLI const user = await chat.apiGetActiveUser() if (!user) { console.log("no user profile") return } console.log(`Bot profile: ${user.profile.displayName} (${user.profile.fullName})`) + // creates or uses the existing long-term address for the bot const address = (await chat.apiGetUserAddress()) || (await chat.apiCreateUserAddress()) console.log(`Bot address: ${address}`) - await chat.sendChatCmdStr("/auto_accept on") + // enables automatic acceptance of contact connections + await chat.addressAutoAccept(true) await processMessages(chat) async function processMessages(chat) { @@ -22,6 +25,7 @@ async function run() { const resp = r instanceof Promise ? await r : r switch (resp.type) { case "contactConnected": { + // sends welcome message when the new contact is connected const {contact} = resp console.log(`${contact.profile.displayName} connected`) await chat.apiSendTextMessage( @@ -32,6 +36,7 @@ async function run() { continue } case "newChatItem": { + // calculates the square of the number and sends the reply const {chatInfo} = resp.chatItem if (chatInfo.type !== ChatInfoType.Direct) continue const msg = ciContentText(resp.chatItem.chatItem.content) diff --git a/packages/simplex-chat-client/typescript/src/client.ts b/packages/simplex-chat-client/typescript/src/client.ts index 825a081429..3ed1ed5b68 100644 --- a/packages/simplex-chat-client/typescript/src/client.ts +++ b/packages/simplex-chat-client/typescript/src/client.ts @@ -1,7 +1,7 @@ import {ABQueue} from "./queue" import {ChatTransport, ChatServer, ChatSrvRequest, ChatSrvResponse, ChatResponseError, localServer, noop} from "./transport" -import {ChatCommand, ChatType} from "./command" -import {ChatResponse} from "./response" +import {ChatCommand, ChatType, Profile} from "./command" +import {ChatResponse, ChatInfo} from "./response" import * as CC from "./command" import * as CR from "./response" @@ -118,14 +118,37 @@ export class ChatClient { } } + async apiStopChat(): Promise { + const r = await this.sendChatCommand({type: "apiStopChat"}) + if (r.type !== "chatStopped") { + throw new ChatCommandError("error stopping chat", r) + } + } + + apiSetIncognito(incognito: boolean): Promise { + return this.okChatCommand({type: "setIncognito", incognito}) + } + + async addressAutoAccept(autoAccept: boolean, autoReply: CC.MsgContent): Promise { + const r = await this.sendChatCommand({type: "addressAutoAccept", autoAccept, autoReply}) + if (r.type !== "userContactLinkUpdated") { + throw new ChatCommandError("error changing user contact address mode", r) + } + } + async apiGetChats(): Promise { const r = await this.sendChatCommand({type: "apiGetChats"}) if (r.type === "apiChats") return r.chats throw new ChatCommandError("error loading chats", r) } - async apiGetChat(chatType: ChatType, chatId: number, pagination: CC.ChatPagination = {count: 100}): Promise { - const r = await this.sendChatCommand({type: "apiGetChat", chatType, chatId, pagination}) + async apiGetChat( + chatType: ChatType, + chatId: number, + pagination: CC.ChatPagination = {count: 100}, + search: string | undefined = undefined + ): Promise { + const r = await this.sendChatCommand({type: "apiGetChat", chatType, chatId, pagination, search}) if (r.type === "apiChat") return r.chat throw new ChatCommandError("error loading chat", r) } @@ -152,18 +175,6 @@ export class ChatClient { throw new ChatCommandError("error deleting chat item", r) } - // func getUserSMPServers() throws -> [String] { - // let r = chatSendCmdSync(.getUserSMPServers) - // if case let .userSMPServers(smpServers) = r { return smpServers } - // throw r - // } - - // func setUserSMPServers(smpServers: [String]) async throws { - // let r = await chatSendCmd(.setUserSMPServers(smpServers: smpServers)) - // if case .cmdOk = r { return } - // throw r - // } - async apiCreateLink(): Promise { const r = await this.sendChatCommand({type: "addContact"}) if (r.type === "invitation") return r.connReqInvitation @@ -184,7 +195,24 @@ export class ChatClient { async apiDeleteChat(chatType: ChatType, chatId: number): Promise { const r = await this.sendChatCommand({type: "apiDeleteChat", chatType, chatId}) - if (r.type !== "contactDeleted") throw new ChatCommandError("error deleting chat", r) + switch (chatType) { + case ChatType.Direct: + if (r.type === "contactDeleted") return + break + case ChatType.Group: + if (r.type === "groupDeletedUser") return + break + case ChatType.ContactRequest: + if (r.type === "contactConnectionDeleted") return + break + } + throw new ChatCommandError("error deleting chat", r) + } + + async apiClearChat(chatType: ChatType, chatId: number): Promise { + const r = await this.sendChatCommand({type: "apiClearChat", chatType, chatId}) + if (r.type === "chatCleared") return r.chatInfo + throw new ChatCommandError("error clearing chat", r) } async apiUpdateProfile(profile: CC.Profile): Promise { @@ -199,11 +227,11 @@ export class ChatClient { } } - // func apiParseMarkdown(text: String) throws -> [FormattedText]? { - // let r = chatSendCmdSync(.apiParseMarkdown(text: text)) - // if case let .apiParsedMarkdown(formattedText) = r { return formattedText } - // throw r - // } + async apiSetContactAlias(contactId: number, localAlias: string): Promise { + const r = await this.sendChatCommand({type: "apiSetContactAlias", contactId, localAlias}) + if (r.type === "contactAliasUpdated") return r.toContact + throw new ChatCommandError("error updating contact alias", r) + } async apiCreateUserAddress(): Promise { const r = await this.sendChatCommand({type: "createMyAddress"}) @@ -246,6 +274,66 @@ export class ChatClient { return this.okChatCommand({type: "apiChatRead", chatType, chatId, itemRange}) } + async apiContactInfo(contactId: number): Promise<[CR.ConnectionStats?, Profile?]> { + const r = await this.sendChatCommand({type: "apiContactInfo", contactId}) + if (r.type === "contactInfo") return [r.connectionStats, r.customUserProfile] + throw new ChatCommandError("error getting contact info", r) + } + + async apiGroupMemberInfo(groupId: number, memberId: number): Promise { + const r = await this.sendChatCommand({type: "apiGroupMemberInfo", groupId, memberId}) + if (r.type === "groupMemberInfo") return r.connectionStats_ + throw new ChatCommandError("error getting group info", r) + } + + async apiReceiveFile(fileId: number): Promise { + const r = await this.sendChatCommand({type: "receiveFile", fileId}) + if (r.type === "rcvFileAccepted") return r.chatItem + throw new ChatCommandError("error receiving file", r) + } + + async apiNewGroup(groupProfile: CR.GroupProfile): Promise { + const r = await this.sendChatCommand({type: "newGroup", groupProfile}) + if (r.type === "groupCreated") return r.groupInfo + throw new ChatCommandError("error creating group", r) + } + + async apiAddMember(groupId: number, contactId: number, memberRole: CC.GroupMemberRole): Promise { + const r = await this.sendChatCommand({type: "apiAddMember", groupId, contactId, memberRole}) + if (r.type === "sentGroupInvitation") return r.member + throw new ChatCommandError("error adding member", r) + } + + async apiJoinGroup(groupId: number): Promise { + const r = await this.sendChatCommand({type: "apiJoinGroup", groupId}) + if (r.type === "userAcceptedGroupSent") return r.groupInfo + throw new ChatCommandError("error joining group", r) + } + + async apiRemoveMember(groupId: number, memberId: number): Promise { + const r = await this.sendChatCommand({type: "apiRemoveMember", groupId, memberId}) + if (r.type === "userDeletedMember") return r.member + throw new ChatCommandError("error removing member", r) + } + + async apiLeaveGroup(groupId: number): Promise { + const r = await this.sendChatCommand({type: "apiLeaveGroup", groupId}) + if (r.type === "leftMemberUser") return r.groupInfo + throw new ChatCommandError("error leaving group", r) + } + + async apiListMembers(groupId: number): Promise { + const r = await this.sendChatCommand({type: "apiListMembers", groupId}) + if (r.type === "groupMembers") return r.group.members + throw new ChatCommandError("error getting group members", r) + } + + async apiUpdateGroup(groupId: number, groupProfile: CR.GroupProfile): Promise { + const r = await this.sendChatCommand({type: "apiUpdateGroupProfile", groupId, groupProfile}) + if (r.type === "groupUpdated") return r.toGroup + throw new ChatCommandError("error updating group", r) + } + private async okChatCommand(command: ChatCommand): Promise { const r = await this.sendChatCommand(command) if (r.type !== "cmdOk") throw new ChatCommandError(`${command.type} command error`, r) diff --git a/packages/simplex-chat-client/typescript/src/command.ts b/packages/simplex-chat-client/typescript/src/command.ts index 3266bf3456..241ce2f0e9 100644 --- a/packages/simplex-chat-client/typescript/src/command.ts +++ b/packages/simplex-chat-client/typescript/src/command.ts @@ -2,7 +2,12 @@ export type ChatCommand = | ShowActiveUser | CreateActiveUser | StartChat + | APIStopChat | SetFilesFolder + | SetIncognito + | APIExportArchive + | APIImportArchive + | APIDeleteStorage | APIGetChats | APIGetChat | APISendMessage @@ -10,12 +15,23 @@ export type ChatCommand = | APIDeleteChatItem | APIChatRead | APIDeleteChat + | APIClearChat | APIAcceptContact | APIRejectContact | APIUpdateProfile + | APISetContactAlias | APIParseMarkdown + | NewGroup + | APIAddMember + | APIJoinGroup + | APIRemoveMember + | APILeaveGroup + | APIListMembers + | APIUpdateGroupProfile | GetUserSMPServers | SetUserSMPServers + | APIContactInfo + | APIGroupMemberInfo | AddContact | Connect | ConnectSimplex @@ -23,12 +39,48 @@ export type ChatCommand = | DeleteMyAddress | ShowMyAddress | AddressAutoAccept + | ReceiveFile + | CancelFile + | FileStatus + +// not included commands (they are not needed for Websocket clients, and can still be sent as strings): +// APIActivateChat +// APISuspendChat +// ResubscribeAllConnections +// APIGetChatItems - not implemented +// APISendCallInvitation +// APIRejectCall +// APISendCallOffer +// APISendCallAnswer +// APISendCallExtraInfo +// APIEndCall +// APIGetCallInvitations +// APICallStatus +// APIGetNtfToken +// APIRegisterToken +// APIVerifyToken +// APIDeleteToken +// APIGetNtfMessage +// APIMemberRole -- not implemented +// ListContacts +// ListGroups +// APISetNetworkConfig +// APIGetNetworkConfig +// APISetChatSettings +// ShowMessages +// LastMessages +// SendMessageBroadcast type ChatCommandTag = | "showActiveUser" | "createActiveUser" | "startChat" + | "apiStopChat" | "setFilesFolder" + | "setIncognito" + | "apiExportArchive" + | "apiImportArchive" + | "apiDeleteStorage" | "apiGetChats" | "apiGetChat" | "apiSendMessage" @@ -36,12 +88,23 @@ type ChatCommandTag = | "apiDeleteChatItem" | "apiChatRead" | "apiDeleteChat" + | "apiClearChat" | "apiAcceptContact" | "apiRejectContact" | "apiUpdateProfile" + | "apiSetContactAlias" | "apiParseMarkdown" + | "newGroup" + | "apiAddMember" + | "apiJoinGroup" + | "apiRemoveMember" + | "apiLeaveGroup" + | "apiListMembers" + | "apiUpdateGroupProfile" | "getUserSMPServers" | "setUserSMPServers" + | "apiContactInfo" + | "apiGroupMemberInfo" | "addContact" | "connect" | "connectSimplex" @@ -49,6 +112,9 @@ type ChatCommandTag = | "deleteMyAddress" | "showMyAddress" | "addressAutoAccept" + | "receiveFile" + | "cancelFile" + | "fileStatus" interface IChatCommand { type: ChatCommandTag @@ -65,6 +131,11 @@ export interface CreateActiveUser extends IChatCommand { export interface StartChat extends IChatCommand { type: "startChat" + subscribeConnections?: boolean +} + +export interface APIStopChat extends IChatCommand { + type: "apiStopChat" } export interface SetFilesFolder extends IChatCommand { @@ -72,8 +143,28 @@ export interface SetFilesFolder extends IChatCommand { filePath: string } +export interface SetIncognito extends IChatCommand { + type: "setIncognito" + incognito: boolean +} + +export interface APIExportArchive extends IChatCommand { + type: "apiExportArchive" + config: ArchiveConfig +} + +export interface APIImportArchive extends IChatCommand { + type: "apiImportArchive" + config: ArchiveConfig +} + +export interface APIDeleteStorage extends IChatCommand { + type: "apiDeleteStorage" +} + export interface APIGetChats extends IChatCommand { type: "apiGetChats" + pendingConnections?: boolean } export interface APIGetChat extends IChatCommand { @@ -81,6 +172,7 @@ export interface APIGetChat extends IChatCommand { chatType: ChatType chatId: number pagination: ChatPagination + search?: string } export interface APISendMessage extends IChatCommand { @@ -130,6 +222,12 @@ export interface APIDeleteChat extends IChatCommand { chatId: number } +export interface APIClearChat extends IChatCommand { + type: "apiClearChat" + chatType: ChatType + chatId: number +} + export interface APIAcceptContact extends IChatCommand { type: "apiAcceptContact" contactReqId: number @@ -145,11 +243,56 @@ export interface APIUpdateProfile extends IChatCommand { profile: Profile } +export interface APISetContactAlias extends IChatCommand { + type: "apiSetContactAlias" + contactId: number + localAlias: string +} + export interface APIParseMarkdown extends IChatCommand { type: "apiParseMarkdown" text: string } +export interface NewGroup extends IChatCommand { + type: "newGroup" + groupProfile: GroupProfile +} + +export interface APIAddMember extends IChatCommand { + type: "apiAddMember" + groupId: number + contactId: number + memberRole: GroupMemberRole +} + +export interface APIJoinGroup extends IChatCommand { + type: "apiJoinGroup" + groupId: number +} + +export interface APIRemoveMember extends IChatCommand { + type: "apiRemoveMember" + groupId: number + memberId: number +} + +export interface APILeaveGroup extends IChatCommand { + type: "apiLeaveGroup" + groupId: number +} + +export interface APIListMembers extends IChatCommand { + type: "apiListMembers" + groupId: number +} + +export interface APIUpdateGroupProfile extends IChatCommand { + type: "apiUpdateGroupProfile" + groupId: number + groupProfile: GroupProfile +} + export interface GetUserSMPServers extends IChatCommand { type: "getUserSMPServers" } @@ -159,6 +302,17 @@ export interface SetUserSMPServers extends IChatCommand { servers: [string] } +export interface APIContactInfo extends IChatCommand { + type: "apiContactInfo" + contactId: number +} + +export interface APIGroupMemberInfo extends IChatCommand { + type: "apiGroupMemberInfo" + groupId: number + memberId: number +} + export interface AddContact extends IChatCommand { type: "addContact" } @@ -186,7 +340,24 @@ export interface ShowMyAddress extends IChatCommand { export interface AddressAutoAccept extends IChatCommand { type: "addressAutoAccept" - enable: boolean + autoAccept: boolean + autoReply?: MsgContent +} + +export interface ReceiveFile extends IChatCommand { + type: "receiveFile" + fileId: number + filePath?: string +} + +export interface CancelFile extends IChatCommand { + type: "cancelFile" + fileId: number +} + +export interface FileStatus extends IChatCommand { + type: "fileStatus" + fileId: number } export interface Profile { @@ -208,12 +379,13 @@ export type ChatPagination = export type ChatItemId = number -type MsgContentTag = "text" | "link" | "images" +type MsgContentTag = "text" | "link" | "image" | "file" -export type MsgContent = MCText | MCUnknown +export type MsgContent = MCText | MCLink | MCImage | MCFile | MCUnknown interface MC { type: MsgContentTag + text: string } interface MCText extends MC { @@ -221,9 +393,32 @@ interface MCText extends MC { text: string } +interface MCLink extends MC { + type: "link" + text: string + preview: LinkPreview +} + +interface MCImage extends MC { + type: "image" + image: string // image preview as base64 encoded data string +} + +interface MCFile extends MC { + type: "file" + text: string +} + interface MCUnknown { type: string - text?: string + text: string +} + +interface LinkPreview { + uri: string + title: string + description: string + image: string } export enum DeleteMode { @@ -231,6 +426,24 @@ export enum DeleteMode { Internal = "internal", } +interface ArchiveConfig { + archivePath: string + disableCompression?: boolean + parentTempDirectory?: string +} + +export enum GroupMemberRole { + GRMember = "member", + GRAdmin = "admin", + GROwner = "owner", +} + +interface GroupProfile { + displayName: string + fullName: string // can be empty string + image?: string +} + export function cmdString(cmd: ChatCommand): string { switch (cmd.type) { case "showActiveUser": @@ -238,11 +451,21 @@ export function cmdString(cmd: ChatCommand): string { case "createActiveUser": return `/u ${JSON.stringify(cmd.profile)}` case "startChat": - return "/_start" + return `/_start subscribe=${cmd.subscribeConnections ? "on" : "off"}` + case "apiStopChat": + return "/_stop" case "setFilesFolder": return `/_files_folder ${cmd.filePath}` + case "setIncognito": + return `/incognito ${cmd.incognito ? "on" : "off"}` + case "apiExportArchive": + return `/_db export ${JSON.stringify(cmd.config)}` + case "apiImportArchive": + return `/_db import ${JSON.stringify(cmd.config)}` + case "apiDeleteStorage": + return "/_db delete" case "apiGetChats": - return "/_get chats" + return `/_get chats pcc=${cmd.pendingConnections ? "on" : "off"}` case "apiGetChat": return `/_get chat ${cmd.chatType}${cmd.chatId}${paginationStr(cmd.pagination)}` case "apiSendMessage": @@ -257,18 +480,40 @@ export function cmdString(cmd: ChatCommand): string { } case "apiDeleteChat": return `/_delete ${cmd.chatType}${cmd.chatId}` + case "apiClearChat": + return `/_clear chat ${cmd.chatType}${cmd.chatId}` case "apiAcceptContact": return `/_accept ${cmd.contactReqId}` case "apiRejectContact": return `/_reject ${cmd.contactReqId}` case "apiUpdateProfile": return `/_profile ${JSON.stringify(cmd.profile)}` + case "apiSetContactAlias": + return `/_set alias @${cmd.contactId} ${cmd.localAlias.trim()}` case "apiParseMarkdown": return `/_parse ${cmd.text}` + case "newGroup": + return `/_group ${JSON.stringify(cmd.groupProfile)}` + case "apiAddMember": + return `/_add #${cmd.groupId} ${cmd.contactId} ${cmd.memberRole}` + case "apiJoinGroup": + return `/_join #${cmd.groupId}` + case "apiRemoveMember": + return `/_remove #${cmd.groupId} ${cmd.memberId}` + case "apiLeaveGroup": + return `/_leave #${cmd.groupId}` + case "apiListMembers": + return `/_members #${cmd.groupId}` + case "apiUpdateGroupProfile": + return `/_group_profile #${cmd.groupId} ${JSON.stringify(cmd.groupProfile)}` case "getUserSMPServers": return "/smp_servers" case "setUserSMPServers": return `/smp_servers ${cmd.servers.join(",") || "default"}` + case "apiContactInfo": + return `/_info @${cmd.contactId}` + case "apiGroupMemberInfo": + return `/_info #${cmd.groupId} ${cmd.memberId}` case "addContact": return "/connect" case "connect": @@ -282,7 +527,13 @@ export function cmdString(cmd: ChatCommand): string { case "showMyAddress": return "/show_address" case "addressAutoAccept": - return `/auto_accept ${cmd.enable ? "on" : "off"}` + return `/auto_accept ${cmd.autoAccept ? "on" : "off"}${cmd.autoReply ? " " + JSON.stringify(cmd.autoReply) : ""}` + case "receiveFile": + return `/freceive ${cmd.fileId}${cmd.filePath ? " " + cmd.filePath : ""}` + case "cancelFile": + return `/fcancel ${cmd.fileId}` + case "fileStatus": + return `/fstatus ${cmd.fileId}` } } diff --git a/packages/simplex-chat-client/typescript/src/response.ts b/packages/simplex-chat-client/typescript/src/response.ts index 6b10e865bb..48fc921da1 100644 --- a/packages/simplex-chat-client/typescript/src/response.ts +++ b/packages/simplex-chat-client/typescript/src/response.ts @@ -1,13 +1,16 @@ -import {ChatItemId, MsgContent, DeleteMode, Profile} from "./command" +import {ChatItemId, MsgContent, DeleteMode, Profile, GroupMemberRole} from "./command" export type ChatResponse = | CRActiveUser | CRChatStarted | CRChatRunning + | CRChatStopped | CRApiChats | CRApiChat | CRApiParsedMarkdown | CRUserSMPServers + | CRContactInfo + | CRGroupMemberInfo | CRNewChatItem | CRChatItemStatusUpdated | CRChatItemUpdated @@ -20,11 +23,14 @@ export type ChatResponse = | CRUserProfile | CRUserProfileNoChange | CRUserProfileUpdated + | CRContactAliasUpdated | CRInvitation | CRSentConfirmation | CRSentInvitation | CRContactUpdated + | CRContactsMerged | CRContactDeleted + | CRChatCleared | CRUserContactLinkCreated | CRUserContactLinkDeleted | CRReceivedContactRequest @@ -38,22 +44,72 @@ export type ChatResponse = | CRContactSubscribed | CRContactSubError | CRContactSubSummary + | CRContactsDisconnected + | CRContactsSubscribed + | CRHostConnected + | CRHostDisconnected | CRGroupEmpty + | CRMemberSubError + | CRMemberSubSummary + | CRGroupSubscribed + | CRRcvFileAccepted + | CRRcvFileAcceptedSndCancelled + | CRRcvFileStart + | CRRcvFileComplete + | CRRcvFileCancelled + | CRRcvFileSndCancelled + | CRSndFileStart + | CRSndFileComplete + | CRSndFileCancelled + | CRSndFileRcvCancelled + | CRSndGroupFileCancelled + | CRSndFileSubError + | CRRcvFileSubError | CRPendingSubSummary + | CRGroupCreated + | CRGroupMembers + | CRUserAcceptedGroupSent + | CRUserDeletedMember + | CRSentGroupInvitation + | CRLeftMemberUser + | CRGroupDeletedUser + | CRGroupInvitation + | CRReceivedGroupInvitation + | CRUserJoinedGroup + | CRJoinedGroupMember + | CRJoinedGroupMemberConnecting + | CRConnectedToGroupMember + | CRDeletedMember + | CRDeletedMemberUser + | CRLeftMember + | CRGroupRemoved + | CRGroupDeleted + | CRGroupUpdated | CRUserContactLinkSubscribed | CRUserContactLinkSubError + | CRNewContactConnection + | CRContactConnectionDeleted | CRMessageError | CRChatCmdError | CRChatError +// not included +// CRChatItemDeletedNotFound +// CRBroadcastSent +// CRGroupsList +// CRFileTransferStatus + type ChatResponseTag = | "activeUser" | "chatStarted" | "chatRunning" + | "chatStopped" | "apiChats" | "apiChat" | "apiParsedMarkdown" | "userSMPServers" + | "contactInfo" + | "groupMemberInfo" | "newChatItem" | "chatItemStatusUpdated" | "chatItemUpdated" @@ -68,11 +124,14 @@ type ChatResponseTag = | "userProfile" | "userProfileNoChange" | "userProfileUpdated" + | "contactAliasUpdated" | "invitation" | "sentConfirmation" | "sentInvitation" | "contactUpdated" + | "contactsMerged" | "contactDeleted" + | "chatCleared" | "receivedContactRequest" | "acceptingContactRequest" | "contactAlreadyExists" @@ -84,10 +143,52 @@ type ChatResponseTag = | "contactSubscribed" | "contactSubError" | "contactSubSummary" + | "contactsDisconnected" + | "contactsSubscribed" + | "hostConnected" + | "hostDisconnected" | "groupEmpty" + | "memberSubError" + | "memberSubSummary" + | "groupSubscribed" + | "rcvFileAccepted" + | "rcvFileAcceptedSndCancelled" + | "rcvFileStart" + | "rcvFileComplete" + | "rcvFileCancelled" + | "rcvFileSndCancelled" + | "sndFileStart" + | "sndFileComplete" + | "sndFileCancelled" + | "sndFileRcvCancelled" + | "sndGroupFileCancelled" + | "fileTransferStatus" + | "sndFileSubError" + | "rcvFileSubError" | "pendingSubSummary" + | "groupCreated" + | "groupMembers" + | "userAcceptedGroupSent" + | "userDeletedMember" + | "sentGroupInvitation" + | "leftMemberUser" + | "groupDeletedUser" + | "groupInvitation" + | "receivedGroupInvitation" + | "userJoinedGroup" + | "joinedGroupMember" + | "joinedGroupMemberConnecting" + | "connectedToGroupMember" + | "deletedMember" + | "deletedMemberUser" + | "leftMember" + | "groupRemoved" + | "groupDeleted" + | "groupUpdated" | "userContactLinkSubscribed" | "userContactLinkSubError" + | "newContactConnection" + | "contactConnectionDeleted" | "messageError" | "chatCmdError" | "chatError" @@ -109,6 +210,10 @@ export interface CRChatRunning extends CR { type: "chatRunning" } +export interface CRChatStopped extends CR { + type: "chatStopped" +} + export interface CRApiChats extends CR { type: "apiChats" chats: Chat[] @@ -129,6 +234,20 @@ export interface CRUserSMPServers extends CR { smpServers: string[] } +export interface CRContactInfo extends CR { + type: "contactInfo" + contact: Contact + connectionStats: ConnectionStats + customUserProfile?: Profile +} + +export interface CRGroupMemberInfo extends CR { + type: "groupMemberInfo" + groupInfo: GroupInfo + member: GroupMember + connectionStats_?: ConnectionStats +} + export interface CRNewChatItem extends CR { type: "newChatItem" chatItem: AChatItem @@ -169,6 +288,7 @@ export interface CRUserContactLinkUpdated extends CR { type: "userContactLinkUpdated" connReqContact: string autoAccept: boolean + autoReply?: MsgContent } export interface CRContactRequestRejected extends CR { @@ -191,6 +311,11 @@ export interface CRUserProfileUpdated extends CR { toProfile: Profile } +export interface CRContactAliasUpdated extends CR { + type: "contactAliasUpdated" + toContact: Contact +} + export interface CRInvitation extends CR { type: "invitation" connReqInvitation: string @@ -210,11 +335,22 @@ export interface CRContactUpdated extends CR { toContact: Contact } +export interface CRContactsMerged extends CR { + type: "contactsMerged" + intoContact: Contact + mergedContact: Contact +} + export interface CRContactDeleted extends CR { type: "contactDeleted" contact: Contact } +export interface CRChatCleared extends CR { + type: "chatCleared" + chatInfo: ChatInfo +} + export interface CRUserContactLinkCreated extends CR { type: "userContactLinkCreated" connReqContact: string @@ -280,16 +416,242 @@ export interface CRContactSubSummary extends CR { contactSubscriptions: ContactSubStatus[] } +export interface CRContactsDisconnected extends CR { + type: "contactsDisconnected" + server: string + contactRefs: ContactRef[] +} + +export interface CRContactsSubscribed extends CR { + type: "contactsSubscribed" + server: string + contactRefs: ContactRef[] +} + +export interface CRHostConnected extends CR { + type: "hostConnected" + protocol: string + transportHost: string +} + +export interface CRHostDisconnected extends CR { + type: "hostDisconnected" + protocol: string + transportHost: string +} + export interface CRGroupEmpty extends CR { type: "groupEmpty" groupInfo: GroupInfo } +export interface CRMemberSubError extends CR { + type: "memberSubError" + groupInfo: GroupInfo + member: GroupMember + chatError: ChatError +} + +export interface CRMemberSubSummary extends CR { + type: "memberSubSummary" + memberSubscriptions: MemberSubStatus[] +} + +export interface CRGroupSubscribed extends CR { + type: "groupSubscribed" + groupInfo: GroupInfo +} + +export interface CRRcvFileAccepted extends CR { + type: "rcvFileAccepted" + chatItem: AChatItem +} + +export interface CRRcvFileAcceptedSndCancelled extends CR { + type: "rcvFileAcceptedSndCancelled" + rcvFileTransfer: RcvFileTransfer +} + +export interface CRRcvFileStart extends CR { + type: "rcvFileStart" + chatItem: AChatItem +} + +export interface CRRcvFileComplete extends CR { + type: "rcvFileComplete" + chatItem: AChatItem +} + +export interface CRRcvFileCancelled extends CR { + type: "rcvFileCancelled" + rcvFileTransfer: RcvFileTransfer +} + +export interface CRRcvFileSndCancelled extends CR { + type: "rcvFileSndCancelled" + rcvFileTransfer: RcvFileTransfer +} + +export interface CRSndFileStart extends CR { + type: "sndFileStart" + chatItem: AChatItem + sndFileTransfer: SndFileTransfer +} + +export interface CRSndFileComplete extends CR { + type: "sndFileComplete" + chatItem: AChatItem + sndFileTransfer: SndFileTransfer +} + +export interface CRSndFileCancelled extends CR { + type: "sndFileCancelled" + chatItem: AChatItem + sndFileTransfer: SndFileTransfer +} + +export interface CRSndFileRcvCancelled extends CR { + type: "sndFileRcvCancelled" + chatItem: AChatItem + sndFileTransfer: SndFileTransfer +} + +export interface CRSndGroupFileCancelled extends CR { + type: "sndGroupFileCancelled" + chatItem: AChatItem + fileTransferMeta: FileTransferMeta + sndFileTransfers: SndFileTransfer[] +} + +export interface CRSndFileSubError extends CR { + type: "sndFileSubError" + sndFileTransfer: SndFileTransfer + chatError: ChatError +} + +export interface CRRcvFileSubError extends CR { + type: "rcvFileSubError" + rcvFileTransfer: RcvFileTransfer + chatError: ChatError +} + export interface CRPendingSubSummary extends CR { type: "pendingSubSummary" pendingSubStatus: PendingSubStatus[] } +export interface CRGroupCreated extends CR { + type: "groupCreated" + groupInfo: GroupInfo +} + +export interface CRGroupMembers extends CR { + type: "groupMembers" + group: Group +} + +export interface CRUserAcceptedGroupSent extends CR { + type: "userAcceptedGroupSent" + groupInfo: GroupInfo +} + +export interface CRUserDeletedMember extends CR { + type: "userDeletedMember" + groupInfo: GroupInfo + member: GroupMember +} + +export interface CRSentGroupInvitation extends CR { + type: "sentGroupInvitation" + groupInfo: GroupInfo + contact: Contact + member: GroupMember +} + +export interface CRLeftMemberUser extends CR { + type: "leftMemberUser" + groupInfo: GroupInfo +} + +export interface CRGroupDeletedUser extends CR { + type: "groupDeletedUser" + groupInfo: GroupInfo +} + +export interface CRGroupInvitation extends CR { + type: "groupInvitation" + groupInfo: GroupInfo +} + +export interface CRReceivedGroupInvitation extends CR { + type: "receivedGroupInvitation" + groupInfo: GroupInfo + contact: Contact + memberRole: GroupMemberRole +} + +export interface CRUserJoinedGroup extends CR { + type: "userJoinedGroup" + groupInfo: GroupInfo + hostMember: GroupMember +} + +export interface CRJoinedGroupMember extends CR { + type: "joinedGroupMember" + groupInfo: GroupInfo + member: GroupMember +} + +export interface CRJoinedGroupMemberConnecting extends CR { + type: "joinedGroupMemberConnecting" + groupInfo: GroupInfo + hostMember: GroupMember + member: GroupMember +} + +export interface CRConnectedToGroupMember extends CR { + type: "connectedToGroupMember" + groupInfo: GroupInfo + member: GroupMember +} + +export interface CRDeletedMember extends CR { + type: "deletedMember" + groupInfo: GroupInfo + byMember: GroupMember + deletedMember: GroupMember +} + +export interface CRDeletedMemberUser extends CR { + type: "deletedMemberUser" + groupInfo: GroupInfo + member: GroupMember +} + +export interface CRLeftMember extends CR { + type: "leftMember" + groupInfo: GroupInfo + member: GroupMember +} + +export interface CRGroupRemoved extends CR { + type: "groupRemoved" + groupInfo: GroupInfo +} + +export interface CRGroupDeleted extends CR { + type: "groupDeleted" + groupInfo: GroupInfo + member: GroupMember +} + +export interface CRGroupUpdated extends CR { + type: "groupUpdated" + fromGroup: GroupInfo + toGroup: GroupInfo + member_?: GroupMember +} + export interface CRUserContactLinkSubscribed extends CR { type: "userContactLinkSubscribed" } @@ -299,6 +661,16 @@ export interface CRUserContactLinkSubError extends CR { chatError: ChatError } +export interface CRNewContactConnection extends CR { + type: "newContactConnection" + connection: PendingContactConnection +} + +export interface CRContactConnectionDeleted extends CR { + type: "contactConnectionDeleted" + connection: PendingContactConnection +} + export interface CRMessageError extends CR { type: "messageError" severity: string @@ -365,6 +737,16 @@ export interface Contact { createdAt: Date } +export interface ContactRef { + contactId: number + localDisplayName: string +} + +export interface Group { + groupInfo: GroupInfo + members: GroupMember[] +} + export interface GroupInfo { groupId: number localDisplayName: string @@ -382,7 +764,7 @@ export interface GroupProfile { export interface GroupMember { groupMemberId: number memberId: string - // memberRole: GroupMemberRole + memberRole: GroupMemberRole // memberCategory: GroupMemberCategory // memberStatus: GroupMemberStatus // invitedBy: InvitedBy @@ -498,7 +880,36 @@ export function ciContentText(content: CIContent): string | undefined { } } -interface RcvFileTransfer {} +interface RcvFileTransfer { + fileId: number + // fileInvitation: FileInvitation + // fileStatus: RcvFileStatus + senderDisplayName: string + chunkSize: number + cancelled: boolean + grpMemberId?: number +} + +interface SndFileTransfer { + fileId: number + fileName: string + filePath: string + fileSize: number + chunkSize: number + recipientDisplayName: string + connId: number + // agentConnId: string + // fileStatus: FileStatus +} + +interface FileTransferMeta { + fileId: number + fileName: string + filePath: string + fileSize: number + chunkSize: number + cancelled: boolean +} export interface ChatStats { unreadCount: number @@ -580,6 +991,18 @@ interface ContactSubStatus {} interface PendingSubStatus {} +export interface ConnectionStats { + rcvServers?: string[] + sndServers?: string[] +} + +interface PendingContactConnection {} + +interface MemberSubStatus { + member: GroupMember + memberError?: ChatError +} + interface AgentErrorType { type: string [x: string]: any