Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4724669bce | |||
| 292c334460 | |||
| dafdf66ada | |||
| 38424af48e | |||
| 88a33990b7 | |||
| 7ce305e16f | |||
| 1d1ba8607e | |||
| 9f6385f763 | |||
| a68b591029 | |||
| 711207743b | |||
| a8a7bb3c99 | |||
| 228c118714 | |||
| 0b86402ce3 | |||
| 2295f7a92b | |||
| 8e03eefa9b | |||
| 53040dbe1d | |||
| 6d5b5ab44f | |||
| 0a18985e68 | |||
| 047aa7deef | |||
| 945ed3f7cb | |||
| e29ea99d2c | |||
| 3b19aaf1d1 | |||
| 15a91278d6 | |||
| cb602dd377 | |||
| 7e2f365c1c | |||
| 8425be0612 | |||
| c0199a38fd | |||
| d97a8c1934 | |||
| 7c36ee7955 | |||
| 55dde3531e | |||
| c3a8ae1eb5 | |||
| edc9560d36 | |||
| 37cfb93217 | |||
| 28ee40074a | |||
| 0ba4598ca2 | |||
| ecb5b0fdeb | |||
| 6cf23f1fd1 | |||
| b86f034c0b | |||
| ce3d7f21b0 | |||
| b38d5f3465 | |||
| a5ad0b185c | |||
| 4f5e135992 | |||
| 50d83d2374 | |||
| 64381be91d | |||
| f47494e5c8 | |||
| 32a105bac8 | |||
| 65b17c9d18 | |||
| aef159b097 | |||
| d29a6542de | |||
| aef697e30a | |||
| fca063e131 | |||
| 8a859044cb | |||
| 895e3878f9 | |||
| b2556e3306 | |||
| eebc24086b | |||
| 94bbc44960 | |||
| 78712541f0 | |||
| 2b4bdf39fb | |||
| a8faaef54e | |||
| 44bad8e093 | |||
| a988ab84f9 | |||
| 85e2013639 | |||
| 1978801561 | |||
| 95a4da71cb | |||
| f13a65ca85 | |||
| e87be44134 | |||
| fb8dfa02f2 | |||
| 67e0ca28a9 | |||
| 7438db0a7d | |||
| b47f064115 | |||
| d9afc47993 | |||
| fcee108863 | |||
| 5a74b8066f | |||
| 809a87ce61 | |||
| c2c05816f3 | |||
| cc4fff0ae5 | |||
| be537f3a24 | |||
| 7498cd4432 | |||
| 5e545b639f | |||
| 1093b01e7e | |||
| 44845ad563 | |||
| 1bfa7f1104 | |||
| 79658b3d8d | |||
| 962287c439 | |||
| ea89c9d8c8 | |||
| 7c723213c2 | |||
| f29614058a | |||
| 8033c8648b | |||
| 3160a9559a | |||
| 74cb3a3cc0 | |||
| f2735020e3 | |||
| 81f29d679b | |||
| a7703209f2 | |||
| 6e48fe3f72 | |||
| 29b683329d | |||
| e7f9e5a834 | |||
| 66ab5bc424 | |||
| 279f8c5453 | |||
| 0e91f10851 | |||
| 4856f6e3e4 | |||
| 0ccf431002 | |||
| 433200bab9 | |||
| 9513a47860 | |||
| 96176936e6 | |||
| 20e7feb953 | |||
| 7fa671f829 | |||
| 1c2e49ae83 | |||
| 2e56b3cb58 | |||
| 642cec3092 | |||
| 1564424f0d | |||
| 177c007edc | |||
| d279c144a6 | |||
| ba2378e5d6 | |||
| b7b393b993 | |||
| d5e66e2284 | |||
| 2ce3cd2fad | |||
| e4328cb98d | |||
| 498181b2e9 | |||
| 6c8fb9e6d0 | |||
| e5f13adc2a | |||
| d9b3742f62 | |||
| 800a4f90bf | |||
| deaea44024 | |||
| 23468f0afd |
@@ -0,0 +1 @@
|
||||
* @epoberezkin @efim-poberezkin
|
||||
@@ -1 +1,2 @@
|
||||
github: simplex-chat
|
||||
open_collective: simplex-chat
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"template": "${{UNCATEGORIZED}}",
|
||||
"pr_template": "- ${{TITLE}}\n"
|
||||
"template": "Commits:\n${{UNCATEGORIZED}}",
|
||||
"pr_template": "- ${{TITLE}}"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- v4
|
||||
- stable
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
@@ -49,24 +49,15 @@ jobs:
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
cache_path: ~/.stack
|
||||
stack_args: "--test"
|
||||
artifact_rel_path: /bin/simplex-chat
|
||||
asset_name: simplex-chat-ubuntu-20_04-x86-64
|
||||
- os: ubuntu-18.04
|
||||
cache_path: ~/.stack
|
||||
stack_args: "--test"
|
||||
artifact_rel_path: /bin/simplex-chat
|
||||
asset_name: simplex-chat-ubuntu-18_04-x86-64
|
||||
- os: macos-latest
|
||||
cache_path: ~/.stack
|
||||
stack_args: "--test"
|
||||
artifact_rel_path: /bin/simplex-chat
|
||||
asset_name: simplex-chat-macos-x86-64
|
||||
# TODO enable tests for windows once fixed (remove stack_args altogether)
|
||||
- os: windows-latest
|
||||
cache_path: C:/sr
|
||||
stack_args: ""
|
||||
artifact_rel_path: /bin/simplex-chat.exe
|
||||
asset_name: simplex-chat-windows-x86-64
|
||||
steps:
|
||||
- name: Clone project
|
||||
@@ -75,7 +66,7 @@ jobs:
|
||||
- name: Setup Stack
|
||||
uses: haskell/actions/setup@v1
|
||||
with:
|
||||
ghc-version: '8.8.4'
|
||||
ghc-version: '8.10.7'
|
||||
enable-stack: true
|
||||
stack-version: 'latest'
|
||||
|
||||
@@ -85,17 +76,49 @@ jobs:
|
||||
path: ${{ matrix.cache_path }}
|
||||
key: ${{ matrix.os }}-${{ hashFiles('stack.yaml') }}
|
||||
|
||||
- name: Build & test
|
||||
id: build_test
|
||||
# / Unix
|
||||
|
||||
- name: Unix build
|
||||
id: unix_build
|
||||
if: matrix.os != 'windows-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
stack build ${{ matrix.stack_args }}
|
||||
stack build --test
|
||||
echo "::set-output name=LOCAL_INSTALL_ROOT::$(stack path --local-install-root)"
|
||||
|
||||
- name: Upload binaries to release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
- name: Unix upload binary to release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest'
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ steps.build_test.outputs.LOCAL_INSTALL_ROOT }}${{ matrix.artifact_rel_path }}
|
||||
file: ${{ steps.unix_build.outputs.LOCAL_INSTALL_ROOT }}/bin/simplex-chat
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
# Unix /
|
||||
|
||||
# / Windows
|
||||
|
||||
# * In powershell multiline commands do not fail if individual commands fail - https://github.community/t/multiline-commands-on-windows-do-not-fail-if-individual-commands-fail/16753
|
||||
# * And GitHub Actions does not support parameterizing shell in a matrix job - https://github.community/t/using-matrix-to-specify-shell-is-it-possible/17065
|
||||
# * So we're running a separate set of actions for Windows build
|
||||
|
||||
# TODO run tests on Windows
|
||||
- name: Windows build
|
||||
id: windows_build
|
||||
if: matrix.os == 'windows-latest'
|
||||
shell: cmd
|
||||
run: |
|
||||
stack build
|
||||
echo "::set-output name=LOCAL_INSTALL_ROOT::$(stack path --local-install-root)"
|
||||
|
||||
- name: Windows upload binary to release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ steps.windows_build.outputs.LOCAL_INSTALL_ROOT }}/bin/simplex-chat.exe
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
# Windows /
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
name: "CLA Assistant"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [opened, closed, synchronize]
|
||||
|
||||
jobs:
|
||||
CLAssistant:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request'
|
||||
# Beta Release
|
||||
uses: cla-assistant/github-action@v2.1.3-beta
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
with:
|
||||
path-to-signatures: 'signatures/v1.1/cla.json'
|
||||
path-to-document: 'https://github.com/simplex-chat/cla/blob/master/CLA.md'
|
||||
# branch should not be protected
|
||||
remote-organization-name: simplex-chat
|
||||
remote-repository-name: cla
|
||||
branch: 'master'
|
||||
# allowlist: user1,bot*
|
||||
|
||||
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
|
||||
#create-file-commit-message: 'For example: Creating file for storing CLA Signatures'
|
||||
#signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo'
|
||||
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
|
||||
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
|
||||
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
|
||||
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
|
||||
#use-dco-flag: true - If you are using DCO instead of CLA
|
||||
@@ -40,7 +40,6 @@ cabal.project.local
|
||||
cabal.project.local~
|
||||
.HTF/
|
||||
.ghc.environment.*
|
||||
*.cabal
|
||||
stack.yaml.lock
|
||||
|
||||
# Idris
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
<img align="right" src="images/logo.svg" alt="SimpleX logo" height="90">
|
||||
<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%">
|
||||
|
||||
# SimpleX chat
|
||||
# SimpleX Chat
|
||||
|
||||
## Private, secure, decentralized
|
||||
SimpleX - the most private and secure open-source chat and applications platform - now with double-ratchet E2E encryption.
|
||||
|
||||
[](https://github.com/simplex-chat/simplex-chat/actions?query=workflow%3Abuild)
|
||||
[](https://github.com/simplex-chat/simplex-chat/releases)
|
||||
[](https://github.com/simplex-chat/simplex-chat/releases)
|
||||
[](https://twitter.com/simplexchat)
|
||||
[](https://www.reddit.com/r/SimpleXChat)
|
||||
|
||||
> **NEW in v0.4: [groups](#groups) and [sending files](#sending-files)!**
|
||||
SimpleX Chat is a terminal (command line) UI using [SimpleXMQ](https://github.com/simplex-chat/simplexmq) message broker.
|
||||
|
||||
The motivation for SimpleX chat is [presented here](./simplex.md).
|
||||
See [SimpleX overview](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information on platform objectives and technical design.
|
||||
|
||||
SimpleX chat prototype is a thin terminal UI on top of [SimpleXMQ](https://github.com/simplex-chat/simplexmq) message broker that uses [SMP protocols](https://github.com/simplex-chat/simplexmq/blob/master/protocol).
|
||||
**v1.0.0 is released: [read announcement here](https://github.com/simplex-chat/simplex-chat/blob/master/blog/20220112-simplex-chat-v1-released.md)**
|
||||
|
||||
See [simplex.chat](https://simplex.chat) website for chat demo and the explanations of the system and how SMP protocol works.
|
||||
### :zap: Quick installation
|
||||
|
||||
```sh
|
||||
curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash
|
||||
```
|
||||
|
||||
Once the chat client is installed, simply run `simplex-chat` from your terminal.
|
||||
|
||||

|
||||
|
||||
@@ -22,8 +31,10 @@ See [simplex.chat](https://simplex.chat) website for chat demo and the explanati
|
||||
- [Disclaimer](#disclaimer)
|
||||
- [Network topology](#network-topology)
|
||||
- [Terminal chat features](#terminal-chat-features)
|
||||
- [Installation](#installation)
|
||||
- [Installation](#🚀-installation)
|
||||
- [Download chat client](#download-chat-client)
|
||||
- [Linux and MacOS](#linux-and-macos)
|
||||
- [Windows](#windows)
|
||||
- [Build from source](#build-from-source)
|
||||
- [Using Docker](#using-docker)
|
||||
- [Using Haskell stack](#using-haskell-stack)
|
||||
@@ -32,25 +43,26 @@ See [simplex.chat](https://simplex.chat) website for chat demo and the explanati
|
||||
- [How to use SimpleX chat](#how-to-use-simplex-chat)
|
||||
- [Groups](#groups)
|
||||
- [Sending files](#sending-files)
|
||||
- [User contact addresses](#user-contact-addresses)
|
||||
- [Access chat history](#access-chat-history)
|
||||
- [Future roadmap](#future-roadmap)
|
||||
- [Roadmap](#Roadmap)
|
||||
- [License](#license)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This is WIP implementation of SimpleX chat that implements a new network topology for asynchronous communication combining the advantages and avoiding the disadvantages of federated and P2P networks.
|
||||
SimpleX Chat implements a new network topology for asynchronous communication combining the advantages and avoiding the disadvantages of federated and P2P networks.
|
||||
|
||||
If you expect a software being reliable most of the time and doing something useful, then this is probably not ready for you yet. We do use it for terminal chat though, and it seems to work most of the time - we would really appreciate if you try it and give us your feedback.
|
||||
[SimpleXMQ security model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) had many improvements in v1.0.0; the implementation has not been audited yet.
|
||||
|
||||
**Please note:** The main differentiation of SimpleX network is the approach to internet message routing rather than encryption; for that reason no sufficient attention was paid to either TCP transport level encryption or to E2E encryption protocols - they are implemented in an ad hoc way based on RSA and AES algorithms. See [SMP protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a) on TCP transport encryption protocol (AEAD-GCM scheme, with an AES key negotiation based on RSA key hash known to the client in advance) and [this section](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2021-01-26-crypto.md#e2e-encryption) on E2E encryption protocol (an ad hoc hybrid scheme a la PGP). These protocols will change in a consumer ready version to something more robust.
|
||||
We use SimpleX Chat all the time, but you may find some bugs. We would really appreciate if you use it and let us know anything that needs to be fixed or improved.
|
||||
|
||||
## Network topology
|
||||
|
||||
SimpleX is a decentralized client-server network that uses redundant, disposable nodes to asynchronously pass the messages via message queues, providing receiver and sender anonymity.
|
||||
SimpleX is a client-server network that uses redundant, disposable nodes to asynchronously pass messages via message queues, providing receiver and sender anonymity.
|
||||
|
||||
Unlike P2P networks, all messages are passed through one or several (for redundancy) servers, that do not even need to have persistence (in fact, the current [SMP server implementation](https://github.com/simplex-chat/simplexmq#smp-server) uses in-memory message storage, persisting only the queue records) - it provides better metadata protection than P2P designs, as no global participant ID is required, and avoids many [problems of P2P networks](https://github.com/simplex-chat/simplex-chat/blob/master/simplex.md#comparison-with-p2p-messaging-protocols).
|
||||
|
||||
Unlike federated networks, the participating server nodes do NOT have records of the users, do NOT communicate with each other, do NOT store messages after they are delivered to the recipients, and there is no way to discover the full list of participating servers - it avoids the problem of metadata visibility that federated networks suffer from and better protects the network, as servers do not communicate with each other. Each server node provides unidirectional "dumb pipes" to the users, that do authorization without authentication, having no knowledge of the the users or their contacts. Each queue is assigned two RSA keys - one for receiver and one for sender - and each queue access is authorized with a signature created using a respective key's private counterpart.
|
||||
Unlike federated networks, the participating server nodes **do not have records of the users**, **do not communicate with each other**, **do not store messages** after they are delivered to the recipients, and there is no way to discover the full list of participating servers. SimpleX network avoids the problem of metadata visibility that federated networks have and better protects the network, as servers do not communicate with each other. Each server node provides unidirectional "dumb pipes" to the users, that do authorization without authentication, having no knowledge of the the users or their contacts. Each queue is assigned two Ed448 keys - one for receiver and one for sender - and each queue access is authorized with a signature created using a respective key's private counterpart.
|
||||
|
||||
The routing of messages relies on the knowledge of client devices how user contacts and groups map at any given moment of time to these disposable queues on server nodes.
|
||||
|
||||
@@ -59,31 +71,49 @@ The routing of messages relies on the knowledge of client devices how user conta
|
||||
- 1-to-1 chat with multiple people in the same terminal window.
|
||||
- Group messaging.
|
||||
- Sending files to contacts and groups.
|
||||
- User contact addresses - establish connections via multiple-use contact links.
|
||||
- Messages persisted in a local SQLite database.
|
||||
- Auto-populated recipient name - just type your messages to reply to the sender once the connection is established.
|
||||
- Demo SMP servers available and pre-configured in the app - or you can [deploy your own server](https://github.com/simplex-chat/simplexmq#using-smp-server-and-smp-agent).
|
||||
- No global identity or any names visible to the server(s), ensuring full privacy of your contacts and conversations.
|
||||
- E2E encryption, with RSA public key that has to be passed out-of-band (see [How to use SimpleX chat](#how-to-use-simplex-chat)).
|
||||
- Message signing and verification with automatically generated RSA keys.
|
||||
- Two layers of E2E encryption (double-ratchet for duplex connections, using X3DH key agreement with ephemeral Curve448 keys, and NaCl crypto_box for SMP queues, using Curve25519 keys) and out-of-band passing of recipient keys (see [How to use SimpleX chat](#how-to-use-simplex-chat)).
|
||||
- Message integrity validation (via including the digests of the previous messages).
|
||||
- Authentication of each command/message by SMP servers with automatically generated RSA key pairs.
|
||||
- TCP transport encryption using SMP transport protocol.
|
||||
- Authentication of each command/message by SMP servers with automatically generated Ed448 keys.
|
||||
- TLS 1.3 transport encryption.
|
||||
- Additional encryption of messages from SMP server to recipient to reduce traffic correlation.
|
||||
|
||||
RSA keys are not used as identity, they are randomly generated for each contact.
|
||||
Public keys involved in key exchange are not used as identity, they are randomly generated for each contact.
|
||||
|
||||
## Installation
|
||||
See [Encryption Primitives Used](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md#encryption-primitives-used) for technical details.
|
||||
|
||||
<a name="🚀-installation"></a>
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### Download chat client
|
||||
|
||||
Download the chat binary for your system from the [latest stable release](https://github.com/simplex-chat/simplex-chat/releases) and make it executable as shown below.
|
||||
|
||||
#### Linux and MacOS
|
||||
|
||||
To **install** or **update** `simplex-chat`, you should run the install script. To do that, use the following cURL or Wget command:
|
||||
|
||||
```sh
|
||||
curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash
|
||||
```
|
||||
|
||||
```sh
|
||||
wget -qO- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash
|
||||
```
|
||||
|
||||
Once the chat client downloads, you can run it with `simplex-chat` command in your terminal.
|
||||
|
||||
Alternatively, you can manually download the chat binary for your system from the [latest stable release](https://github.com/simplex-chat/simplex-chat/releases) and make it executable as shown below.
|
||||
|
||||
```sh
|
||||
chmod +x <binary>
|
||||
mv <binary> ~/.local/bin/simplex-chat
|
||||
```
|
||||
|
||||
(or any other preferred location on PATH).
|
||||
(or any other preferred location on `PATH`).
|
||||
|
||||
On MacOS you also need to [allow Gatekeeper to run it](https://support.apple.com/en-us/HT202491).
|
||||
|
||||
@@ -95,6 +125,8 @@ move <binary> %APPDATA%/local/bin/simplex-chat.exe
|
||||
|
||||
### Build from source
|
||||
|
||||
> **Please note:** to build the app use source code from [stable branch](https://github.com/simplex-chat/simplex-chat/tree/stable).
|
||||
|
||||
#### Using Docker
|
||||
|
||||
On Linux, you can build the chat executable using [docker build with custom output](https://docs.docker.com/engine/reference/commandline/build/#custom-build-outputs):
|
||||
@@ -102,10 +134,11 @@ On Linux, you can build the chat executable using [docker build with custom outp
|
||||
```shell
|
||||
$ git clone git@github.com:simplex-chat/simplex-chat.git
|
||||
$ cd simplex-chat
|
||||
$ git checkout stable
|
||||
$ DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
|
||||
```
|
||||
|
||||
> **Please note:** If you encounter ``version `GLIBC_2.28' not found`` error, rebuild it with `haskell:8.8.4-stretch` base image (change it in your local [Dockerfile](Dockerfile)).
|
||||
> **Please note:** If you encounter `` version `GLIBC_2.28' not found `` error, rebuild it with `haskell:8.10.4-stretch` base image (change it in your local [Dockerfile](Dockerfile)).
|
||||
|
||||
#### Using Haskell stack
|
||||
|
||||
@@ -120,6 +153,7 @@ and build the project:
|
||||
```shell
|
||||
$ git clone git@github.com:simplex-chat/simplex-chat.git
|
||||
$ cd simplex-chat
|
||||
$ git checkout stable
|
||||
$ stack install
|
||||
```
|
||||
|
||||
@@ -129,7 +163,7 @@ $ stack install
|
||||
|
||||
To start the chat client, run `simplex-chat` from the terminal.
|
||||
|
||||
By default, app data directory is created in the home directory (`~/.simplex`, or `%APPDATA%/simplex` on Windows), and two SQLite database files `simplex.chat.db` and `simplex.agent.db` are initialized in it.
|
||||
By default, app data directory is created in the home directory (`~/.simplex`, or `%APPDATA%/simplex` on Windows), and two SQLite database files `simplex_v1_chat.db` and `simplex_v1_agent.db` are initialized in it.
|
||||
|
||||
To specify a different file path prefix for the database files use `-d` command line option:
|
||||
|
||||
@@ -137,17 +171,17 @@ To specify a different file path prefix for the database files use `-d` command
|
||||
$ simplex-chat -d alice
|
||||
```
|
||||
|
||||
Running above, for example, would create `alice.chat.db` and `alice.agent.db` database files in current directory.
|
||||
Running above, for example, would create `alice_v1_chat.db` and `alice_v1_agent.db` database files in current directory.
|
||||
|
||||
Default SMP servers are hosted on Linode (London, UK and Fremont, CA) - they are [pre-configured in the app](https://github.com/simplex-chat/simplex-chat/blob/master/src/Simplex/Chat/Options.hs#L40). Base-64 encoded string after server host is the transport key digest.
|
||||
Three default SMP servers are hosted on Linode - they are [pre-configured in the app](https://github.com/simplex-chat/simplex-chat/blob/master/src/Simplex/Chat/Options.hs#L42).
|
||||
|
||||
If you deployed your own SMP server(s) you can configure client via `-s` option:
|
||||
|
||||
```shell
|
||||
$ simplex-chat -s smp.example.com:5223#KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8=
|
||||
$ simplex-chat -s smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@smp.example.com
|
||||
```
|
||||
|
||||
The base-64 encoded string in server address is the digest of RSA transport handshake key that the server will generate on the first run and output its digest.
|
||||
Base64url encoded string preceding the server address is the server's offline certificate fingerprint which is validated by client during TLS handshake.
|
||||
|
||||
You can still talk to people using default or any other server - it only affects the location of the message queue when you initiate the connection (and the reply queue can be on another server, as set by the other party's client).
|
||||
|
||||
@@ -155,9 +189,9 @@ Run `simplex-chat -h` to see all available options.
|
||||
|
||||
### How to use SimpleX chat
|
||||
|
||||
Once you have started the chat, you will be prompted to specify your "display name" and an optional "full name" to create a local chat profile. Your display name is an alias for your contacts to refer to you by - it is not unique and does not serve as a global identity. In case different contacts chose the same display name, the chat client adds a numeric suffix to their local display names.
|
||||
Once you have started the chat, you will be prompted to specify your "display name" and an optional "full name" to create a local chat profile. Your display name is an alias for your contacts to refer to you by - it is not unique and does not serve as a global identity. If some of your contacts chose the same display name, the chat client adds a numeric suffix to their local display name.
|
||||
|
||||
This diagram shows how to connect and message a contact:
|
||||
The diagram below shows how to connect and message a contact:
|
||||
|
||||
<div align="center">
|
||||
<img align="center" src="images/how-to-use-simplex.svg">
|
||||
@@ -167,7 +201,7 @@ Once you've set up your local profile, enter `/c` (for `/connect`) to create a n
|
||||
|
||||
You are able to create multiple invitations by entering `/connect` multiple times and sending these invitations to the corresponding contacts you'd like to connect with.
|
||||
|
||||
The invitation has the format `smp::<server>::<queue_id>::<rsa_public_key_for_this_queue_only>`. The invitation can only be used once and even if this is intercepted, the attacker would not be able to use it to send you the messages via this queue once your contact confirms that the connection is established.
|
||||
The invitation can only be used once and even if this is intercepted, the attacker would not be able to use it to send you the messages via this queue once your contact confirms that the connection is established. See agent protocol for explanation of [invitation format](https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md#connection-request).
|
||||
|
||||
The contact who received the invitation should enter `/c <invitation>` to accept the connection. This establishes the connection, and both parties are notified.
|
||||
|
||||
@@ -177,7 +211,7 @@ Use `/help` in chat to see the list of available commands.
|
||||
|
||||
### Groups
|
||||
|
||||
To create a group use `/g <group>`, then add contacts to it with `/a <group> <name>`and send messages with `#<group> <message>`. Use `/help groups` for other commands.
|
||||
To create a group use `/g <group>`, then add contacts to it with `/a <group> <name>`. You can then send messages to the group by entering `#<group> <message>`. Use `/help groups` for other commands.
|
||||
|
||||

|
||||
|
||||
@@ -191,39 +225,62 @@ You can send a file to your contact with `/f @<contact> <file_path>` - the recip
|
||||
|
||||
You can send files to a group with `/f #<group> <file_path>`.
|
||||
|
||||
### User contact addresses
|
||||
|
||||
As an alternative to one-time invitation links, you can create a long-term address with `/ad` (for `/address`). The created address can then be shared via any channel, and used by other users as a link to make a contact request with `/c <user_contact_address>`.
|
||||
|
||||
You can accept or reject incoming requests with `/ac <name>` and `/rc <name>` commands.
|
||||
|
||||
User address is "long-term" in a sense that it is a multiple-use connection link - it can be used until it is deleted by the user, in which case all established connections would still remain active (unlike how it works with email, when changing the address results in people not being able to message you).
|
||||
|
||||
Use `/help address` for other commands.
|
||||
|
||||

|
||||
|
||||
### Access chat history
|
||||
|
||||
> 🚧 **Section currently out of date - will be updated soon** 🏗
|
||||
SimpleX chat stores all your contacts and conversations in a local SQLite database, making it private and portable by design, owned and controlled by user.
|
||||
|
||||
SimpleX chat stores all your contacts and conversations in a local database file, making it private and portable by design, fully owned and controlled by you.
|
||||
You can view and search your chat history by querying your database. Run the below script to create message views in your database.
|
||||
|
||||
You can search your chat history via SQLite database file:
|
||||
|
||||
```
|
||||
sqlite3 ~/.simplex/smp-chat.db
|
||||
```sh
|
||||
curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/message_views.sql | sqlite3 ~/.simplex/simplex_v1_chat.db
|
||||
```
|
||||
|
||||
Now you can query `messages` table, for example:
|
||||
Open SQLite Command Line Shell:
|
||||
|
||||
```sql
|
||||
select * from messages
|
||||
where conn_alias = cast('alice' as blob)
|
||||
and body like '%cats%'
|
||||
order by internal_id desc;
|
||||
```sh
|
||||
sqlite3 ~/.simplex/simplex_v1_chat.db
|
||||
```
|
||||
|
||||
See [Message queries](./message_queries.md) for examples.
|
||||
|
||||
> **Please note:** SQLite foreign key constraints are disabled by default, and must be **[enabled separately for each database connection](https://sqlite.org/foreignkeys.html#fk_enable)**. The latter can be achieved by running `PRAGMA foreign_keys = ON;` command on an open database connection. By running data altering queries without enabling foreign keys prior to that, you may risk putting your database in an inconsistent state.
|
||||
|
||||
## Future roadmap
|
||||
**Convenience queries**
|
||||
|
||||
Get all messages from today (`chat_dt` is in UTC):
|
||||
|
||||
```sql
|
||||
select * from all_messages_plain where date(chat_dt) > date('now', '-1 day') order by chat_dt;
|
||||
```
|
||||
|
||||
Get overnight messages in the morning:
|
||||
|
||||
```sql
|
||||
select * from all_messages_plain where chat_dt > datetime('now', '-15 hours') order by chat_dt;
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
1. Mobile and desktop apps (in progress).
|
||||
2. SMP protocol improvements:
|
||||
- SMP queue redundancy and rotation.
|
||||
- Message delivery confirmation.
|
||||
- Support multiple devices.
|
||||
- SMP queue redundancy and rotation.
|
||||
- Message delivery confirmation.
|
||||
- Support multiple devices.
|
||||
3. Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages:
|
||||
- keep all your contacts and groups even if you lose the domain.
|
||||
- the server doesn't have information about your contacts and groups.
|
||||
- keep all your contacts and groups even if you lose the domain.
|
||||
- the server doesn't have information about your contacts and groups.
|
||||
4. Media server to optimize sending large files to groups.
|
||||
5. Channels server for large groups and broadcast channels.
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
build/
|
||||
release/
|
||||
debug/
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
@@ -0,0 +1 @@
|
||||
SimpleX
|
||||
@@ -0,0 +1,117 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
<map>
|
||||
<entry key="app/src/main/res/drawable-v24/ic_launcher_foreground.xml" value="0.2328125" />
|
||||
<entry key="app/src/main/res/drawable/ic_launcher_background.xml" value="0.2328125" />
|
||||
<entry key="app/src/main/res/layout/activity_main.xml" value="0.22010869565217392" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,60 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 32
|
||||
|
||||
defaultConfig {
|
||||
applicationId "chat.simplex.app"
|
||||
minSdk 24
|
||||
targetSdk 32
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
ndk {
|
||||
abiFilters 'arm64-v8a'
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cppFlags ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path file('src/main/cpp/CMakeLists.txt')
|
||||
version '3.10.2'
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
testImplementation 'junit:junit:4.+'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("chat.simplex.app", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="chat.simplex.app">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.SimpleX">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,68 @@
|
||||
# For more information about using CMake with Android Studio, read the
|
||||
# documentation: https://d.android.com/studio/projects/add-native-code.html
|
||||
|
||||
# Sets the minimum version of CMake required to build the native library.
|
||||
|
||||
cmake_minimum_required(VERSION 3.10.2)
|
||||
|
||||
# Declares and names the project.
|
||||
|
||||
project("app")
|
||||
|
||||
# Creates and names a library, sets it as either STATIC
|
||||
# or SHARED, and provides the relative paths to its source code.
|
||||
# You can define multiple libraries, and CMake builds them for you.
|
||||
# Gradle automatically packages shared libraries with your APK.
|
||||
|
||||
add_library( # Sets the name of the library.
|
||||
app-lib
|
||||
|
||||
# Sets the library as a shared library.
|
||||
SHARED
|
||||
|
||||
# Provides a relative path to your source file(s).
|
||||
simplex-api.c)
|
||||
|
||||
# Searches for a specified prebuilt library and stores the path as a
|
||||
# variable. Because CMake includes system libraries in the search path by
|
||||
# default, you only need to specify the name of the public NDK library
|
||||
# you want to add. CMake verifies that the library exists before
|
||||
# completing its build.
|
||||
|
||||
find_library( # Sets the name of the path variable.
|
||||
log-lib
|
||||
|
||||
# Specifies the name of the NDK library that
|
||||
# you want CMake to locate.
|
||||
log)
|
||||
|
||||
find_library( # Sets the name of the path variable.
|
||||
c-lib
|
||||
|
||||
# Specifies the name of the NDK library that
|
||||
# you want CMake to locate.
|
||||
c
|
||||
NAMES libc.so
|
||||
REQUIRED)
|
||||
|
||||
add_library( simplex SHARED IMPORTED )
|
||||
set_target_properties( simplex PROPERTIES IMPORTED_LOCATION
|
||||
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libsimplex.so)
|
||||
|
||||
add_library( support SHARED IMPORTED )
|
||||
set_target_properties( support PROPERTIES IMPORTED_LOCATION
|
||||
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libsupport.so)
|
||||
|
||||
|
||||
# Specifies libraries CMake should link to your target library. You
|
||||
# can link multiple libraries, such as libraries you define in this
|
||||
# build script, prebuilt third-party libraries, or system libraries.
|
||||
|
||||
target_link_libraries( # Specifies the target library.
|
||||
app-lib
|
||||
|
||||
simplex support
|
||||
|
||||
# Links the target library to the log library
|
||||
# included in the NDK.
|
||||
${log-lib})
|
||||
@@ -0,0 +1,72 @@
|
||||
#include <jni.h>
|
||||
|
||||
// from the RTS
|
||||
void hs_init(int * argc, char **argv[]);
|
||||
|
||||
// from android-support
|
||||
void setLineBuffering(void);
|
||||
int pipe_std_to_socket(const char * name);
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_chat_simplex_app_MainActivityKt_pipeStdOutToSocket(JNIEnv *env, __unused jclass clazz, jstring socket_name) {
|
||||
const char *name = (*env)->GetStringUTFChars(env, socket_name, JNI_FALSE);
|
||||
int ret = pipe_std_to_socket(name);
|
||||
(*env)->ReleaseStringUTFChars(env, socket_name, name);
|
||||
return ret;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_chat_simplex_app_MainActivityKt_initHS(__unused JNIEnv *env, __unused jclass clazz) {
|
||||
hs_init(NULL, NULL);
|
||||
setLineBuffering();
|
||||
}
|
||||
|
||||
// from simplex-chat
|
||||
typedef void* chat_store;
|
||||
typedef void* controller;
|
||||
|
||||
extern chat_store chat_init_store(const char * path);
|
||||
extern char *chat_get_user(chat_store store);
|
||||
extern char *chat_create_user(chat_store store, const char *data);
|
||||
extern controller chat_start(chat_store store);
|
||||
extern char *chat_send_cmd(controller ctl, const char *cmd);
|
||||
extern char *chat_recv_msg(controller ctl);
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_chat_simplex_app_MainActivityKt_chatInit(JNIEnv *env, __unused jclass clazz, jstring datadir) {
|
||||
const char *_data = (*env)->GetStringUTFChars(env, datadir, JNI_FALSE);
|
||||
jlong res = (jlong)chat_init_store(_data);
|
||||
(*env)->ReleaseStringUTFChars(env, datadir, _data);
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_app_MainActivityKt_chatGetUser(JNIEnv *env, __unused jclass clazz, jlong controller) {
|
||||
return (*env)->NewStringUTF(env, chat_get_user((void*)controller));
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_app_MainActivityKt_chatCreateUser(JNIEnv *env, __unused jclass clazz, jlong controller, jstring data) {
|
||||
const char *_data = (*env)->GetStringUTFChars(env, data, JNI_FALSE);
|
||||
jstring res = (*env)->NewStringUTF(env, chat_create_user((void*)controller, _data));
|
||||
(*env)->ReleaseStringUTFChars(env, data, _data);
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_chat_simplex_app_MainActivityKt_chatStart(JNIEnv *env, jclass clazz, jlong controller) {
|
||||
return (jlong)chat_start((void*)controller);
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_app_MainActivityKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) {
|
||||
const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE);
|
||||
jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg));
|
||||
(*env)->ReleaseStringUTFChars(env, msg, _msg);
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_app_MainActivityKt_chatRecvMsg(JNIEnv *env, __unused jclass clazz, jlong controller) {
|
||||
return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller));
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import android.net.LocalServerSocket
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import java.util.concurrent.Semaphore
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
// ghc's rts
|
||||
external fun initHS()
|
||||
// android-support
|
||||
external fun pipeStdOutToSocket(socketName: String) : Int
|
||||
|
||||
// simplex-chat
|
||||
typealias Store = Long
|
||||
typealias Controller = Long
|
||||
external fun chatInit(filesDir: String): Store
|
||||
external fun chatGetUser(controller: Store) : String
|
||||
external fun chatCreateUser(controller: Store, data: String) : String
|
||||
external fun chatStart(controller: Store) : Controller
|
||||
external fun chatSendCmd(controller: Controller, msg: String) : String
|
||||
external fun chatRecvMsg(controller: Controller) : String
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
weakActivity = WeakReference(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
val store : Store = chatInit(this.applicationContext.filesDir.toString())
|
||||
// create user if needed
|
||||
if(chatGetUser(store) == "{}") {
|
||||
chatCreateUser(store, """
|
||||
{"displayName": "test", "fullName": "android test"}
|
||||
""".trimIndent())
|
||||
}
|
||||
Log.d("SIMPLEX (user)", chatGetUser(store))
|
||||
|
||||
val controller = chatStart(store)
|
||||
|
||||
val cmdinput = this.findViewById<AppCompatEditText>(R.id.cmdInput)
|
||||
|
||||
cmdinput.setOnEditorActionListener { _, actionId, _ ->
|
||||
when (actionId) {
|
||||
EditorInfo.IME_ACTION_SEND -> {
|
||||
Log.d("SIMPLEX SEND", chatSendCmd(controller, cmdinput.text.toString()))
|
||||
cmdinput.text?.clear()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
thread(name="receiver") {
|
||||
val chatlog = FifoQueue<String>(500)
|
||||
while(true) {
|
||||
val msg = chatRecvMsg(controller)
|
||||
Log.d("SIMPLEX RECV", msg)
|
||||
chatlog.add(msg)
|
||||
val currentText = chatlog.joinToString("\n")
|
||||
weakActivity.get()?.runOnUiThread {
|
||||
val log = weakActivity.get()?.findViewById<TextView>(R.id.chatlog)
|
||||
val scroll = weakActivity.get()?.findViewById<ScrollView>(R.id.scroller)
|
||||
log?.text = currentText
|
||||
scroll?.scrollTo(0, scroll.getChildAt(0).height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var weakActivity : WeakReference<MainActivity>
|
||||
init {
|
||||
val socketName = "local.socket.address.listen.native.cmd2"
|
||||
|
||||
val s = Semaphore(0)
|
||||
thread(name="stdout/stderr pipe") {
|
||||
Log.d("SIMPLEX", "starting server")
|
||||
val server = LocalServerSocket(socketName)
|
||||
Log.d("SIMPLEX", "started server")
|
||||
s.release()
|
||||
val receiver = server.accept()
|
||||
Log.d("SIMPLEX", "started receiver")
|
||||
val logbuffer = FifoQueue<String>(500)
|
||||
if (receiver != null) {
|
||||
val inStream = receiver.inputStream
|
||||
val inStreamReader = InputStreamReader(inStream)
|
||||
val input = BufferedReader(inStreamReader)
|
||||
|
||||
while(true) {
|
||||
val line = input.readLine() ?: break
|
||||
Log.d("SIMPLEX (stdout/stderr)", line)
|
||||
logbuffer.add(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.loadLibrary("app-lib")
|
||||
|
||||
s.acquire()
|
||||
pipeStdOutToSocket(socketName)
|
||||
|
||||
initHS()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FifoQueue<E>(private var capacity: Int) : LinkedList<E>() {
|
||||
override fun add(element: E): Boolean {
|
||||
if(size > capacity) removeFirst()
|
||||
return super.add(element)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/cmdInput"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:autofillHints=""
|
||||
android:ems="10"
|
||||
android:imeOptions="actionSend"
|
||||
android:inputType="textPersonName"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:ignore="SpeakableTextPresentCheck" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scroller"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/cmdInput"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chatlog"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,16 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.SimpleX" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">SimpleX</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,16 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.SimpleX" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,17 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:7.0.4"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
@@ -0,0 +1,6 @@
|
||||
#Fri Jan 21 23:13:54 GMT 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,10 @@
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter() // Warning: this repository is going to shut down soon
|
||||
}
|
||||
}
|
||||
rootProject.name = "SimpleX"
|
||||
include ':app'
|
||||
@@ -0,0 +1,67 @@
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
## App packaging
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
# Package.resolved
|
||||
# *.xcodeproj
|
||||
#
|
||||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
||||
# hence it is not needed unless you have added a package configuration file to your project
|
||||
# .swiftpm
|
||||
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# Pods/
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||
# *.xcworkspace
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build/
|
||||
|
||||
# Accio dependency management
|
||||
Dependencies/
|
||||
.accio/
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo.
|
||||
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots/**/*.png
|
||||
fastlane/test_output
|
||||
|
||||
# Code Injection
|
||||
#
|
||||
# After new code Injection tools there's a generated folder /iOSInjectionProject
|
||||
# https://github.com/johnno1962/injectionforxcode
|
||||
|
||||
iOSInjectionProject/
|
||||
|
||||
Libraries/
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"localizable" : true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// Shared
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 17/01/2022.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
|
||||
var body: some View {
|
||||
if let user = chatModel.currentUser {
|
||||
ChatListView(user: user)
|
||||
.onAppear {
|
||||
DispatchQueue.global().async {
|
||||
while(true) {
|
||||
do {
|
||||
try processReceivedMsg(chatModel, chatRecvMsg())
|
||||
} catch {
|
||||
print("error receiving message: ", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
chatModel.chats = try apiGetChats()
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
WelcomeView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//struct ContentView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ContentView(text: "Hello!")
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,383 @@
|
||||
//
|
||||
// ChatModel.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 22/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
final class ChatModel: ObservableObject {
|
||||
@Published var currentUser: User?
|
||||
// list of chat "previews"
|
||||
@Published var chats: [Chat] = []
|
||||
// current chat
|
||||
@Published var chatId: String?
|
||||
@Published var chatItems: [ChatItem] = []
|
||||
// items in the terminal view
|
||||
@Published var terminalItems: [TerminalItem] = []
|
||||
@Published var userAddress: String?
|
||||
@Published var appOpenUrl: URL?
|
||||
@Published var connectViaUrl = false
|
||||
|
||||
func hasChat(_ id: String) -> Bool {
|
||||
chats.first(where: { $0.id == id }) != nil
|
||||
}
|
||||
|
||||
func getChat(_ id: String) -> Chat? {
|
||||
chats.first(where: { $0.id == id })
|
||||
}
|
||||
|
||||
func addChat(_ chat: Chat) {
|
||||
withAnimation {
|
||||
chats.insert(chat, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
func updateChatInfo(_ cInfo: ChatInfo) {
|
||||
if let ix = chats.firstIndex(where: { $0.id == cInfo.id }) {
|
||||
chats[ix].chatInfo = cInfo
|
||||
}
|
||||
}
|
||||
|
||||
func replaceChat(_ id: String, _ chat: Chat) {
|
||||
if let ix = chats.firstIndex(where: { $0.id == id }) {
|
||||
chats[ix] = chat
|
||||
} else {
|
||||
// invalid state, correcting
|
||||
chats.insert(chat, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
if let ix = chats.firstIndex(where: { $0.id == cInfo.id }) {
|
||||
chats[ix].chatItems = [cItem]
|
||||
if chatId != cInfo.id {
|
||||
let chat = chats.remove(at: ix)
|
||||
chats.insert(chat, at: 0)
|
||||
}
|
||||
}
|
||||
if chatId == cInfo.id {
|
||||
chatItems.append(cItem)
|
||||
}
|
||||
}
|
||||
|
||||
func removeChat(_ id: String) {
|
||||
withAnimation {
|
||||
chats.removeAll(where: { $0.id == id })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct User: Decodable {
|
||||
var userId: Int64
|
||||
var userContactId: Int64
|
||||
var localDisplayName: ContactName
|
||||
var profile: Profile
|
||||
var activeUser: Bool
|
||||
|
||||
// internal init(userId: Int64, userContactId: Int64, localDisplayName: ContactName, profile: Profile, activeUser: Bool) {
|
||||
// self.userId = userId
|
||||
// self.userContactId = userContactId
|
||||
// self.localDisplayName = localDisplayName
|
||||
// self.profile = profile
|
||||
// self.activeUser = activeUser
|
||||
// }
|
||||
}
|
||||
|
||||
let sampleUser = User(
|
||||
userId: 1,
|
||||
userContactId: 1,
|
||||
localDisplayName: "alice",
|
||||
profile: sampleProfile,
|
||||
activeUser: true
|
||||
)
|
||||
|
||||
typealias ContactName = String
|
||||
|
||||
typealias GroupName = String
|
||||
|
||||
struct Profile: Codable {
|
||||
var displayName: String
|
||||
var fullName: String
|
||||
}
|
||||
|
||||
let sampleProfile = Profile(
|
||||
displayName: "alice",
|
||||
fullName: "Alice"
|
||||
)
|
||||
|
||||
enum ChatType: String {
|
||||
case direct = "@"
|
||||
case group = "#"
|
||||
case contactRequest = "<@"
|
||||
}
|
||||
|
||||
enum ChatInfo: Identifiable, Decodable {
|
||||
case direct(contact: Contact)
|
||||
case group(groupInfo: GroupInfo)
|
||||
case contactRequest(contactRequest: UserContactRequest)
|
||||
|
||||
var localDisplayName: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return "@\(contact.localDisplayName)"
|
||||
case let .group(groupInfo): return "#\(groupInfo.localDisplayName)"
|
||||
case let .contactRequest(contactRequest): return "< @\(contactRequest.localDisplayName)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var id: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.id
|
||||
case let .group(groupInfo): return groupInfo.id
|
||||
case let .contactRequest(contactRequest): return contactRequest.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var chatType: ChatType {
|
||||
get {
|
||||
switch self {
|
||||
case .direct: return .direct
|
||||
case .group: return .group
|
||||
case .contactRequest: return .contactRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var apiId: Int64 {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.apiId
|
||||
case let .group(groupInfo): return groupInfo.apiId
|
||||
case let .contactRequest(contactRequest): return contactRequest.apiId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sampleDirectChatInfo = ChatInfo.direct(contact: sampleContact)
|
||||
|
||||
let sampleGroupChatInfo = ChatInfo.group(groupInfo: sampleGroupInfo)
|
||||
|
||||
let sampleContactRequestChatInfo = ChatInfo.contactRequest(contactRequest: sampleContactRequest)
|
||||
|
||||
final class Chat: ObservableObject, Identifiable {
|
||||
@Published var chatInfo: ChatInfo
|
||||
@Published var chatItems: [ChatItem]
|
||||
|
||||
init(_ cData: ChatData) {
|
||||
self.chatInfo = cData.chatInfo
|
||||
self.chatItems = cData.chatItems
|
||||
}
|
||||
|
||||
init(chatInfo: ChatInfo, chatItems: [ChatItem] = []) {
|
||||
self.chatInfo = chatInfo
|
||||
self.chatItems = chatItems
|
||||
}
|
||||
|
||||
var id: String { get { chatInfo.id } }
|
||||
}
|
||||
|
||||
struct ChatData: Decodable, Identifiable {
|
||||
var chatInfo: ChatInfo
|
||||
var chatItems: [ChatItem]
|
||||
|
||||
var id: String { get { chatInfo.id } }
|
||||
}
|
||||
|
||||
struct Contact: Identifiable, Decodable {
|
||||
var contactId: Int64
|
||||
var localDisplayName: ContactName
|
||||
var profile: Profile
|
||||
var activeConn: Connection
|
||||
var viaGroup: Int64?
|
||||
|
||||
var id: String { get { "@\(contactId)" } }
|
||||
var apiId: Int64 { get { contactId } }
|
||||
var connected: Bool { get { activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready" } }
|
||||
}
|
||||
|
||||
let sampleContact = Contact(
|
||||
contactId: 1,
|
||||
localDisplayName: "alice",
|
||||
profile: sampleProfile,
|
||||
activeConn: sampleConnection
|
||||
)
|
||||
|
||||
struct Connection: Decodable {
|
||||
var connStatus: String
|
||||
}
|
||||
|
||||
let sampleConnection = Connection(connStatus: "ready")
|
||||
|
||||
struct UserContactRequest: Decodable {
|
||||
var contactRequestId: Int64
|
||||
var localDisplayName: ContactName
|
||||
var profile: Profile
|
||||
|
||||
var id: String { get { "<@\(contactRequestId)" } }
|
||||
|
||||
var apiId: Int64 { get { contactRequestId } }
|
||||
}
|
||||
|
||||
let sampleContactRequest = UserContactRequest(
|
||||
contactRequestId: 1,
|
||||
localDisplayName: "alice",
|
||||
profile: sampleProfile
|
||||
)
|
||||
|
||||
struct GroupInfo: Identifiable, Decodable {
|
||||
var groupId: Int64
|
||||
var localDisplayName: GroupName
|
||||
var groupProfile: GroupProfile
|
||||
|
||||
var id: String { get { "#\(groupId)" } }
|
||||
|
||||
var apiId: Int64 { get { groupId } }
|
||||
}
|
||||
|
||||
let sampleGroupInfo = GroupInfo(
|
||||
groupId: 1,
|
||||
localDisplayName: "team",
|
||||
groupProfile: sampleGroupProfile
|
||||
)
|
||||
|
||||
struct GroupProfile: Codable {
|
||||
var displayName: String
|
||||
var fullName: String
|
||||
}
|
||||
|
||||
let sampleGroupProfile = GroupProfile(
|
||||
displayName: "team",
|
||||
fullName: "My Team"
|
||||
)
|
||||
|
||||
struct GroupMember: Decodable {
|
||||
|
||||
}
|
||||
|
||||
struct AChatItem: Decodable {
|
||||
var chatInfo: ChatInfo
|
||||
var chatItem: ChatItem
|
||||
}
|
||||
|
||||
struct ChatItem: Identifiable, Decodable {
|
||||
var chatDir: CIDirection
|
||||
var meta: CIMeta
|
||||
var content: CIContent
|
||||
|
||||
var id: Int64 { get { meta.itemId } }
|
||||
}
|
||||
|
||||
func chatItemSample(_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String) -> ChatItem {
|
||||
ChatItem(
|
||||
chatDir: dir,
|
||||
meta: ciMetaSample(id, ts, text),
|
||||
content: .sndMsgContent(msgContent: .text(text))
|
||||
)
|
||||
}
|
||||
|
||||
enum CIDirection: Decodable {
|
||||
case directSnd
|
||||
case directRcv
|
||||
case groupSnd
|
||||
case groupRcv(GroupMember)
|
||||
|
||||
var sent: Bool {
|
||||
get {
|
||||
switch self {
|
||||
case .directSnd: return true
|
||||
case .directRcv: return false
|
||||
case .groupSnd: return true
|
||||
case .groupRcv: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CIMeta: Decodable {
|
||||
var itemId: Int64
|
||||
var itemTs: Date
|
||||
var itemText: String
|
||||
var createdAt: Date
|
||||
}
|
||||
|
||||
func ciMetaSample(_ id: Int64, _ ts: Date, _ text: String) -> CIMeta {
|
||||
CIMeta(
|
||||
itemId: id,
|
||||
itemTs: ts,
|
||||
itemText: text,
|
||||
createdAt: ts
|
||||
)
|
||||
}
|
||||
|
||||
enum CIContent: Decodable {
|
||||
case sndMsgContent(msgContent: MsgContent)
|
||||
case rcvMsgContent(msgContent: MsgContent)
|
||||
// files etc.
|
||||
|
||||
var text: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .sndMsgContent(mc): return mc.string
|
||||
case let .rcvMsgContent(mc): return mc.string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum MsgContent {
|
||||
case text(String)
|
||||
case unknown(type: String, text: String)
|
||||
case invalid(error: String)
|
||||
|
||||
var string: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .text(text): return text
|
||||
case let .unknown(_, text): return text
|
||||
case .invalid: return "invalid"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cmdString: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .text(text): return "text \(text)"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case text
|
||||
}
|
||||
}
|
||||
|
||||
extension MsgContent: Decodable {
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
do {
|
||||
let type = try container.decode(String.self, forKey: CodingKeys.type)
|
||||
switch type {
|
||||
case "text":
|
||||
let text = try container.decode(String.self, forKey: CodingKeys.text)
|
||||
self = .text(text)
|
||||
default:
|
||||
let text = try? container.decode(String.self, forKey: CodingKeys.text)
|
||||
self = .unknown(type: type, text: text ?? "unknown message format")
|
||||
}
|
||||
} catch {
|
||||
self = .invalid(error: String(describing: error))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// JSON.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 29/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
func getJSONDecoder() -> JSONDecoder {
|
||||
let jd = JSONDecoder()
|
||||
let fracSeconds = getDateFormatter("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ")
|
||||
let noFracSeconds = getDateFormatter("yyyy-MM-dd'T'HH:mm:ssZZZZZ")
|
||||
jd.dateDecodingStrategy = .custom { decoder in
|
||||
let container = try decoder.singleValueContainer()
|
||||
let string = try container.decode(String.self)
|
||||
if let date = fracSeconds.date(from: string) ?? noFracSeconds.date(from: string) {
|
||||
return date
|
||||
}
|
||||
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
|
||||
}
|
||||
return jd
|
||||
}
|
||||
|
||||
func getJSONEncoder() -> JSONEncoder {
|
||||
let je = JSONEncoder()
|
||||
je.dateEncodingStrategy = .iso8601
|
||||
return je
|
||||
}
|
||||
|
||||
private func getDateFormatter(_ format: String) -> DateFormatter {
|
||||
let df = DateFormatter()
|
||||
df.locale = Locale(identifier: "en_US_POSIX")
|
||||
df.dateFormat = format
|
||||
df.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
return df
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
//
|
||||
// ChatAPI.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 27/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
private var chatStore: chat_store?
|
||||
private var chatController: chat_ctrl?
|
||||
private let jsonDecoder = getJSONDecoder()
|
||||
private let jsonEncoder = getJSONEncoder()
|
||||
|
||||
enum ChatCommand {
|
||||
case apiGetChats
|
||||
case apiGetChat(type: ChatType, id: Int64)
|
||||
case apiSendMessage(type: ChatType, id: Int64, msg: MsgContent)
|
||||
case addContact
|
||||
case connect(connReq: String)
|
||||
case apiDeleteChat(type: ChatType, id: Int64)
|
||||
case apiUpdateProfile(profile: Profile)
|
||||
case createMyAddress
|
||||
case deleteMyAddress
|
||||
case showMyAddress
|
||||
case apiAcceptContact(contactReqId: Int64)
|
||||
case apiRejectContact(contactReqId: Int64)
|
||||
case string(String)
|
||||
|
||||
var cmdString: String {
|
||||
get {
|
||||
switch self {
|
||||
case .apiGetChats:
|
||||
return "/_get chats"
|
||||
case let .apiGetChat(type, id):
|
||||
return "/_get chat \(type.rawValue)\(id) count=500"
|
||||
case let .apiSendMessage(type, id, mc):
|
||||
return "/_send \(type.rawValue)\(id) \(mc.cmdString)"
|
||||
case .addContact:
|
||||
return "/connect"
|
||||
case let .connect(connReq):
|
||||
return "/connect \(connReq)"
|
||||
case let .apiDeleteChat(type, id):
|
||||
return "/_delete \(type.rawValue)\(id)"
|
||||
case let .apiUpdateProfile(profile):
|
||||
return "/profile \(profile.displayName) \(profile.fullName)"
|
||||
case .createMyAddress:
|
||||
return "/address"
|
||||
case .deleteMyAddress:
|
||||
return "/delete_address"
|
||||
case .showMyAddress:
|
||||
return "/show_address"
|
||||
case let .apiAcceptContact(contactReqId):
|
||||
return "/_accept \(contactReqId)"
|
||||
case let .apiRejectContact(contactReqId):
|
||||
return "/_reject \(contactReqId)"
|
||||
case let .string(str):
|
||||
return str
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct APIResponse: Decodable {
|
||||
var resp: ChatResponse
|
||||
}
|
||||
|
||||
enum ChatResponse: Decodable, Error {
|
||||
case response(type: String, json: String)
|
||||
case apiChats(chats: [ChatData])
|
||||
case apiChat(chat: ChatData)
|
||||
case invitation(connReqInvitation: String)
|
||||
case sentConfirmation
|
||||
case sentInvitation
|
||||
case contactDeleted(contact: Contact)
|
||||
case userProfileNoChange
|
||||
case userProfileUpdated(fromProfile: Profile, toProfile: Profile)
|
||||
case userContactLink(connReqContact: String)
|
||||
case userContactLinkCreated(connReqContact: String)
|
||||
case userContactLinkDeleted
|
||||
case contactConnected(contact: Contact)
|
||||
case receivedContactRequest(contactRequest: UserContactRequest)
|
||||
case acceptingContactRequest(contact: Contact)
|
||||
case contactRequestRejected
|
||||
case newChatItem(chatItem: AChatItem)
|
||||
case chatCmdError(chatError: ChatError)
|
||||
|
||||
var responseType: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .response(type, _): return "* \(type)"
|
||||
case .apiChats: return "apiChats"
|
||||
case .apiChat: return "apiChat"
|
||||
case .invitation: return "invitation"
|
||||
case .sentConfirmation: return "sentConfirmation"
|
||||
case .sentInvitation: return "sentInvitation"
|
||||
case .contactDeleted: return "contactDeleted"
|
||||
case .userProfileNoChange: return "userProfileNoChange"
|
||||
case .userProfileUpdated: return "userProfileNoChange"
|
||||
case .userContactLink: return "userContactLink"
|
||||
case .userContactLinkCreated: return "userContactLinkCreated"
|
||||
case .userContactLinkDeleted: return "userContactLinkDeleted"
|
||||
case .contactConnected: return "contactConnected"
|
||||
case .receivedContactRequest: return "receivedContactRequest"
|
||||
case .acceptingContactRequest: return "acceptingContactRequest"
|
||||
case .contactRequestRejected: return "contactRequestRejected"
|
||||
case .newChatItem: return "newChatItem"
|
||||
case .chatCmdError: return "chatCmdError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var details: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .response(_, json): return json
|
||||
case let .apiChats(chats): return String(describing: chats)
|
||||
case let .apiChat(chat): return String(describing: chat)
|
||||
case let .invitation(connReqInvitation): return connReqInvitation
|
||||
case .sentConfirmation: return noDetails
|
||||
case .sentInvitation: return noDetails
|
||||
case let .contactDeleted(contact): return String(describing: contact)
|
||||
case .userProfileNoChange: return noDetails
|
||||
case let .userProfileUpdated(_, toProfile): return String(describing: toProfile)
|
||||
case let .userContactLink(connReq): return connReq
|
||||
case let .userContactLinkCreated(connReq): return connReq
|
||||
case .userContactLinkDeleted: return noDetails
|
||||
case let .contactConnected(contact): return String(describing: contact)
|
||||
case let .receivedContactRequest(contactRequest): return String(describing: contactRequest)
|
||||
case let .acceptingContactRequest(contact): return String(describing: contact)
|
||||
case .contactRequestRejected: return noDetails
|
||||
case let .newChatItem(chatItem): return String(describing: chatItem)
|
||||
case let .chatCmdError(chatError): return String(describing: chatError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var noDetails: String { get { "\(responseType): no details" } }
|
||||
}
|
||||
|
||||
enum TerminalItem: Identifiable {
|
||||
case cmd(Date, ChatCommand)
|
||||
case resp(Date, ChatResponse)
|
||||
|
||||
var id: Date {
|
||||
get {
|
||||
switch self {
|
||||
case let .cmd(id, _): return id
|
||||
case let .resp(id, _): return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var label: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .cmd(_, cmd): return "> \(cmd.cmdString.prefix(30))"
|
||||
case let .resp(_, resp): return "< \(resp.responseType)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var details: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .cmd(_, cmd): return cmd.cmdString
|
||||
case let .resp(_, resp): return resp.details
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func chatGetUser() -> User? {
|
||||
let store = getStore()
|
||||
print("chatGetUser")
|
||||
let r: UserResponse? = decodeCJSON(chat_get_user(store))
|
||||
let user = r?.user
|
||||
if user != nil { initChatCtrl(store) }
|
||||
print("user", user as Any)
|
||||
return user
|
||||
}
|
||||
|
||||
func chatCreateUser(_ p: Profile) -> User? {
|
||||
let store = getStore()
|
||||
print("chatCreateUser")
|
||||
var str = encodeCJSON(p)
|
||||
chat_create_user(store, &str)
|
||||
let user = chatGetUser()
|
||||
if user != nil { initChatCtrl(store) }
|
||||
print("user", user as Any)
|
||||
return user
|
||||
}
|
||||
|
||||
func chatSendCmd(_ cmd: ChatCommand) throws -> ChatResponse {
|
||||
var c = cmd.cmdString.cString(using: .utf8)!
|
||||
print("command", cmd.cmdString)
|
||||
// TODO some mechanism to update model without passing it - maybe Publisher / Subscriber?
|
||||
// DispatchQueue.main.async {
|
||||
// termId += 1
|
||||
// chatModel.terminalItems.append(.cmd(termId, cmd))
|
||||
// }
|
||||
return chatResponse(chat_send_cmd(getChatCtrl(), &c)!)
|
||||
}
|
||||
|
||||
func chatRecvMsg() throws -> ChatResponse {
|
||||
chatResponse(chat_recv_msg(getChatCtrl())!)
|
||||
}
|
||||
|
||||
func apiGetChats() throws -> [Chat] {
|
||||
let r = try chatSendCmd(.apiGetChats)
|
||||
if case let .apiChats(chats) = r { return chats.map { Chat.init($0) } }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetChat(type: ChatType, id: Int64) throws -> Chat {
|
||||
let r = try chatSendCmd(.apiGetChat(type: type, id: id))
|
||||
if case let .apiChat(chat) = r { return Chat.init(chat) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSendMessage(type: ChatType, id: Int64, msg: MsgContent) throws -> ChatItem {
|
||||
let r = try chatSendCmd(.apiSendMessage(type: type, id: id, msg: msg))
|
||||
if case let .newChatItem(aChatItem) = r { return aChatItem.chatItem }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiAddContact() throws -> String {
|
||||
let r = try chatSendCmd(.addContact)
|
||||
if case let .invitation(connReqInvitation) = r { return connReqInvitation }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiConnect(connReq: String) throws {
|
||||
let r = try chatSendCmd(.connect(connReq: connReq))
|
||||
switch r {
|
||||
case .sentConfirmation: return
|
||||
case .sentInvitation: return
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
|
||||
func apiDeleteChat(type: ChatType, id: Int64) throws {
|
||||
let r = try chatSendCmd(.apiDeleteChat(type: type, id: id))
|
||||
if case .contactDeleted = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiUpdateProfile(profile: Profile) throws -> Profile? {
|
||||
let r = try chatSendCmd(.apiUpdateProfile(profile: profile))
|
||||
switch r {
|
||||
case .userProfileNoChange: return nil
|
||||
case let .userProfileUpdated(_, toProfile): return toProfile
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
|
||||
func apiCreateUserAddress() throws -> String {
|
||||
let r = try chatSendCmd(.createMyAddress)
|
||||
if case let .userContactLinkCreated(connReq) = r { return connReq }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteUserAddress() throws {
|
||||
let r = try chatSendCmd(.deleteMyAddress)
|
||||
if case .userContactLinkDeleted = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetUserAddress() throws -> String? {
|
||||
let r = try chatSendCmd(.showMyAddress)
|
||||
switch r {
|
||||
case let .userContactLink(connReq):
|
||||
return connReq
|
||||
case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)):
|
||||
return nil
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
|
||||
func apiAcceptContactRequest(contactReqId: Int64) throws -> Contact {
|
||||
let r = try chatSendCmd(.apiAcceptContact(contactReqId: contactReqId))
|
||||
if case let .acceptingContactRequest(contact) = r { return contact }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiRejectContactRequest(contactReqId: Int64) throws {
|
||||
let r = try chatSendCmd(.apiRejectContact(contactReqId: contactReqId))
|
||||
if case .contactRequestRejected = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func processReceivedMsg(_ chatModel: ChatModel, _ res: ChatResponse) {
|
||||
DispatchQueue.main.async {
|
||||
chatModel.terminalItems.append(.resp(Date.now, res))
|
||||
switch res {
|
||||
case let .contactConnected(contact):
|
||||
let cInfo = ChatInfo.direct(contact: contact)
|
||||
if chatModel.hasChat(contact.id) {
|
||||
chatModel.updateChatInfo(cInfo)
|
||||
} else {
|
||||
chatModel.addChat(Chat(chatInfo: cInfo, chatItems: []))
|
||||
}
|
||||
case let .receivedContactRequest(contactRequest):
|
||||
chatModel.addChat(Chat(
|
||||
chatInfo: ChatInfo.contactRequest(contactRequest: contactRequest),
|
||||
chatItems: []
|
||||
))
|
||||
case let .newChatItem(aChatItem):
|
||||
chatModel.addChatItem(aChatItem.chatInfo, aChatItem.chatItem)
|
||||
default:
|
||||
print("unsupported response: ", res.responseType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct UserResponse: Decodable {
|
||||
var user: User?
|
||||
var error: String?
|
||||
}
|
||||
|
||||
private func chatResponse(_ cjson: UnsafePointer<CChar>) -> ChatResponse {
|
||||
let s = String.init(cString: cjson)
|
||||
let d = s.data(using: .utf8)!
|
||||
// TODO is there a way to do it without copying the data? e.g:
|
||||
// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson))
|
||||
// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free)
|
||||
|
||||
// TODO some mechanism to update model without passing it - maybe Publisher / Subscriber?
|
||||
|
||||
do {
|
||||
let r = try jsonDecoder.decode(APIResponse.self, from: d)
|
||||
return r.resp
|
||||
} catch {
|
||||
print (error)
|
||||
}
|
||||
|
||||
var type: String?
|
||||
var json: String?
|
||||
if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary {
|
||||
if let j1 = j["resp"] as? NSDictionary, j1.count == 1 {
|
||||
type = j1.allKeys[0] as? String
|
||||
}
|
||||
json = prettyJSON(j)
|
||||
}
|
||||
return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
|
||||
}
|
||||
|
||||
func prettyJSON(_ obj: NSDictionary) -> String? {
|
||||
if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) {
|
||||
return String(decoding: d, as: UTF8.self)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func getStore() -> chat_store {
|
||||
if let store = chatStore { return store }
|
||||
let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path + "/mobile_v1"
|
||||
var cstr = dataDir.cString(using: .utf8)!
|
||||
chatStore = chat_init_store(&cstr)
|
||||
return chatStore!
|
||||
}
|
||||
|
||||
private func initChatCtrl(_ store: chat_store) {
|
||||
if chatController == nil {
|
||||
chatController = chat_start(store)
|
||||
}
|
||||
}
|
||||
|
||||
private func getChatCtrl() -> chat_ctrl {
|
||||
if let controller = chatController { return controller }
|
||||
fatalError("Chat controller was not started!")
|
||||
}
|
||||
|
||||
private func decodeCJSON<T: Decodable>(_ cjson: UnsafePointer<CChar>) -> T? {
|
||||
let s = String.init(cString: cjson)
|
||||
let d = s.data(using: .utf8)!
|
||||
// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson))
|
||||
// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free)
|
||||
return try? jsonDecoder.decode(T.self, from: d)
|
||||
}
|
||||
|
||||
private func getJSONObject(_ cjson: UnsafePointer<CChar>) -> NSDictionary? {
|
||||
let s = String.init(cString: cjson)
|
||||
let d = s.data(using: .utf8)!
|
||||
return try? JSONSerialization.jsonObject(with: d) as? NSDictionary
|
||||
}
|
||||
|
||||
private func encodeCJSON<T: Encodable>(_ value: T) -> [CChar] {
|
||||
let data = try! jsonEncoder.encode(value)
|
||||
let str = String(decoding: data, as: UTF8.self)
|
||||
return str.cString(using: .utf8)!
|
||||
}
|
||||
|
||||
enum ChatError: Decodable {
|
||||
case error(errorType: ChatErrorType)
|
||||
case errorStore(storeError: StoreError)
|
||||
// TODO other error cases
|
||||
}
|
||||
|
||||
enum ChatErrorType: Decodable {
|
||||
case invalidConnReq
|
||||
}
|
||||
|
||||
enum StoreError: Decodable {
|
||||
case userContactLinkNotFound
|
||||
// TODO other error cases
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import Foundation
|
||||
|
||||
var greeting = "Hello, playground"
|
||||
|
||||
let jsonEncoder = JSONEncoder()
|
||||
|
||||
//jsonDecoder.decode(Test.self, from: "{\"name\":\"hello\",\"id\":1}".data(using: .utf8)!)
|
||||
|
||||
|
||||
var a = [1, 2, 3]
|
||||
|
||||
a.removeAll(where: { $0 == 1} )
|
||||
|
||||
print(a)
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='5.0' target-platform='ios' buildActiveScheme='true' importAppTypes='true'>
|
||||
<timeline fileName='timeline.xctimeline'/>
|
||||
</playground>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Timeline
|
||||
version = "3.0">
|
||||
<TimelineItems>
|
||||
<LoggerValueHistoryTimelineItem
|
||||
documentLocation = "file:///Users/evgeny/opensource/simplex-chat/simplex-chat/apps/ios/Shared/MyPlayground.playground#CharacterRangeLen=88&CharacterRangeLoc=91&EndingColumnNumber=0&EndingLineNumber=7&StartingColumnNumber=3&StartingLineNumber=6&Timestamp=665423482.97412"
|
||||
selectedRepresentationIndex = "0"
|
||||
shouldTrackSuperviewWidth = "NO">
|
||||
</LoggerValueHistoryTimelineItem>
|
||||
</TimelineItems>
|
||||
</Timeline>
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
extern void hs_init(int argc, char **argv[]);
|
||||
|
||||
typedef void* chat_store;
|
||||
typedef void* chat_ctrl;
|
||||
|
||||
extern chat_store chat_init_store(char *path);
|
||||
extern char *chat_get_user(chat_store store);
|
||||
extern char *chat_create_user(chat_store store, char *data);
|
||||
extern chat_ctrl chat_start(chat_store store);
|
||||
extern char *chat_send_cmd(chat_ctrl ctl, char *cmd);
|
||||
extern char *chat_recv_msg(chat_ctrl ctl);
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
extern void hs_init(int argc, char ** argv[]);
|
||||
|
||||
typedef void* chat_store;
|
||||
typedef void* chat_ctrl;
|
||||
|
||||
extern chat_store chat_init_store(char * path);
|
||||
extern char *chat_get_user(chat_store store);
|
||||
extern char *chat_create_user(chat_store store, char *data);
|
||||
extern chat_ctrl chat_start(chat_store store);
|
||||
extern char *chat_send_cmd(chat_ctrl ctl, char *cmd);
|
||||
extern char *chat_recv_msg(chat_ctrl ctl);
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// SimpleXApp.swift
|
||||
// Shared
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 17/01/2022.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct SimpleXApp: App {
|
||||
@StateObject private var chatModel = ChatModel()
|
||||
|
||||
init() {
|
||||
hs_init(0, nil)
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environmentObject(chatModel)
|
||||
.onOpenURL { url in
|
||||
chatModel.appOpenUrl = url
|
||||
chatModel.connectViaUrl = true
|
||||
print(url)
|
||||
}
|
||||
.onAppear() {
|
||||
chatModel.currentUser = chatGetUser()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// ChatItemView.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 30/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
private var dateFormatter: DateFormatter?
|
||||
|
||||
struct ChatItemView: View {
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
let sent = chatItem.chatDir.sent
|
||||
|
||||
return VStack {
|
||||
Group {
|
||||
Text(chatItem.content.text)
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal, 12)
|
||||
.frame(minWidth: 200, maxWidth: 300, alignment: .leading)
|
||||
.foregroundColor(sent ? .white : .primary)
|
||||
.textSelection(.enabled)
|
||||
Text(getDateFormatter().string(from: chatItem.meta.itemTs))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(sent ? .white : .secondary)
|
||||
.padding(.bottom, 8)
|
||||
.padding(.horizontal, 12)
|
||||
.frame(minWidth: 200, maxWidth: 300, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
.background(sent ? .blue : Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(10)
|
||||
.padding(.horizontal)
|
||||
.frame(
|
||||
minWidth: 200,
|
||||
maxWidth: .infinity,
|
||||
minHeight: 0,
|
||||
maxHeight: .infinity,
|
||||
alignment: sent ? .trailing : .leading
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func getDateFormatter() -> DateFormatter {
|
||||
if let df = dateFormatter { return df }
|
||||
let df = DateFormatter()
|
||||
df.dateFormat = "HH:mm"
|
||||
dateFormatter = df
|
||||
return df
|
||||
}
|
||||
|
||||
struct ChatItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group{
|
||||
ChatItemView(chatItem: chatItemSample(1, .directSnd, Date.now, "hello"))
|
||||
ChatItemView(chatItem: chatItemSample(2, .directRcv, Date.now, "hello there too"))
|
||||
}
|
||||
.previewLayout(.fixed(width: 300, height: 70))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// ChatView.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 27/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ChatView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
var chatInfo: ChatInfo
|
||||
@State private var inProgress: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
VStack {
|
||||
ForEach(chatModel.chatItems, id: \.id) {
|
||||
ChatItemView(chatItem: $0)
|
||||
}
|
||||
.onAppear { scrollToBottom(proxy) }
|
||||
.onChange(of: chatModel.chatItems.count) { _ in scrollToBottom(proxy) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
SendMessageView(sendMessage: sendMessage, inProgress: inProgress)
|
||||
}
|
||||
.navigationTitle(chatInfo.localDisplayName)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button { chatModel.chatId = nil } label: {
|
||||
Image(systemName: "chevron.backward")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
|
||||
}
|
||||
|
||||
func scrollToBottom(_ proxy: ScrollViewProxy) {
|
||||
if let id = chatModel.chatItems.last?.id {
|
||||
withAnimation {
|
||||
proxy.scrollTo(id, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(_ msg: String) {
|
||||
do {
|
||||
let chatItem = try apiSendMessage(type: chatInfo.chatType, id: chatInfo.apiId, msg: .text(msg))
|
||||
chatModel.addChatItem(chatInfo, chatItem)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
chatModel.chatId = "@1"
|
||||
chatModel.chatItems = [
|
||||
chatItemSample(1, .directSnd, Date.now, "hello"),
|
||||
chatItemSample(2, .directRcv, Date.now, "hi"),
|
||||
chatItemSample(3, .directRcv, Date.now, "hi there"),
|
||||
chatItemSample(4, .directRcv, Date.now, "hello again"),
|
||||
chatItemSample(5, .directSnd, Date.now, "hi there!!!"),
|
||||
chatItemSample(6, .directSnd, Date.now, "how are you?"),
|
||||
chatItemSample(7, .directSnd, Date.now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
|
||||
]
|
||||
return ChatView(chatInfo: sampleDirectChatInfo)
|
||||
.environmentObject(chatModel)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// SendMessageView.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 29/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SendMessageView: View {
|
||||
var sendMessage: (String) -> Void
|
||||
var inProgress: Bool = false
|
||||
@State var command: String = ""
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
TextField("Message...", text: $command)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.textInputAutocapitalization(.never)
|
||||
.disableAutocorrection(true)
|
||||
.onSubmit(submit)
|
||||
|
||||
if (inProgress) {
|
||||
ProgressView()
|
||||
.frame(width: 40, height: 20, alignment: .center)
|
||||
} else {
|
||||
Button("Send", action :submit)
|
||||
.disabled(command.isEmpty)
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 30)
|
||||
.padding(12)
|
||||
}
|
||||
|
||||
func submit() {
|
||||
sendMessage(command)
|
||||
command = ""
|
||||
}
|
||||
}
|
||||
|
||||
struct SendMessageView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SendMessageView(sendMessage: { print ($0) })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
//
|
||||
// ChatListNavLink.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 01/02/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ChatListNavLink: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State var chat: Chat
|
||||
|
||||
@State private var showDeleteContactAlert = false
|
||||
@State private var showDeleteGroupAlert = false
|
||||
@State private var showContactRequestAlert = false
|
||||
@State private var showContactRequestDialog = false
|
||||
@State private var alertContact: Contact?
|
||||
@State private var alertGroupInfo: GroupInfo?
|
||||
@State private var alertContactRequest: UserContactRequest?
|
||||
|
||||
var body: some View {
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact):
|
||||
contactNavLink(contact)
|
||||
case let .group(groupInfo):
|
||||
groupNavLink(groupInfo)
|
||||
case let .contactRequest(cReq):
|
||||
contactRequestNavLink(cReq)
|
||||
}
|
||||
}
|
||||
|
||||
private func chatView() -> some View {
|
||||
ChatView(chatInfo: chat.chatInfo)
|
||||
.onAppear {
|
||||
do {
|
||||
let cInfo = chat.chatInfo
|
||||
let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId)
|
||||
chatModel.updateChatInfo(chat.chatInfo)
|
||||
chatModel.chatItems = chat.chatItems
|
||||
} catch {
|
||||
print("apiGetChatItems", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func contactNavLink(_ contact: Contact) -> some View {
|
||||
NavigationLink(
|
||||
tag: chat.chatInfo.id,
|
||||
selection: $chatModel.chatId,
|
||||
destination: { chatView() },
|
||||
label: { ChatPreviewView(chat: chat) }
|
||||
)
|
||||
.disabled(!contact.connected)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button(role: .destructive) {
|
||||
alertContact = contact
|
||||
showDeleteContactAlert = true
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showDeleteContactAlert) {
|
||||
deleteContactAlert(alertContact!)
|
||||
}
|
||||
.frame(height: 80)
|
||||
}
|
||||
|
||||
private func groupNavLink(_ groupInfo: GroupInfo) -> some View {
|
||||
NavigationLink(
|
||||
tag: chat.chatInfo.id,
|
||||
selection: $chatModel.chatId,
|
||||
destination: { chatView() },
|
||||
label: { ChatPreviewView(chat: chat) }
|
||||
)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button(role: .destructive) {
|
||||
alertGroupInfo = groupInfo
|
||||
showDeleteGroupAlert = true
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showDeleteGroupAlert) {
|
||||
deleteGroupAlert(alertGroupInfo!)
|
||||
}
|
||||
.frame(height: 80)
|
||||
}
|
||||
|
||||
private func contactRequestNavLink(_ contactRequest: UserContactRequest) -> some View {
|
||||
ContactRequestView(contactRequest: contactRequest)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button { acceptContactRequest(contactRequest) }
|
||||
label: { Label("Accept", systemImage: "checkmark") }
|
||||
.tint(.blue)
|
||||
Button(role: .destructive) {
|
||||
alertContactRequest = contactRequest
|
||||
showContactRequestAlert = true
|
||||
} label: {
|
||||
Label("Reject", systemImage: "multiply")
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showContactRequestAlert) {
|
||||
contactRequestAlert(alertContactRequest!)
|
||||
}
|
||||
.frame(height: 80)
|
||||
.onTapGesture { showContactRequestDialog = true }
|
||||
.confirmationDialog("Connection request", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
|
||||
Button("Accept contact") { acceptContactRequest(contactRequest) }
|
||||
Button("Reject contact (sender NOT notified)") { rejectContactRequest(contactRequest) }
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteContactAlert(_ contact: Contact) -> Alert {
|
||||
Alert(
|
||||
title: Text("Delete contact?"),
|
||||
message: Text("Contact and all messages will be deleted"),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
do {
|
||||
try apiDeleteChat(type: .direct, id: contact.apiId)
|
||||
chatModel.removeChat(contact.id)
|
||||
} catch let error {
|
||||
print("Error: \(error)")
|
||||
}
|
||||
alertContact = nil
|
||||
}, secondaryButton: .cancel() {
|
||||
alertContact = nil
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func deleteGroupAlert(_ groupInfo: GroupInfo) -> Alert {
|
||||
Alert(
|
||||
title: Text("Delete group"),
|
||||
message: Text("Group deletion is not supported")
|
||||
)
|
||||
}
|
||||
|
||||
private func contactRequestAlert(_ contactRequest: UserContactRequest) -> Alert {
|
||||
Alert(
|
||||
title: Text("Reject contact request"),
|
||||
message: Text("The sender will NOT be notified"),
|
||||
primaryButton: .destructive(Text("Reject")) {
|
||||
rejectContactRequest(contactRequest)
|
||||
alertContactRequest = nil
|
||||
}, secondaryButton: .cancel {
|
||||
alertContactRequest = nil
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func acceptContactRequest(_ contactRequest: UserContactRequest) {
|
||||
do {
|
||||
let contact = try apiAcceptContactRequest(contactReqId: contactRequest.apiId)
|
||||
let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: [])
|
||||
chatModel.replaceChat(contactRequest.id, chat)
|
||||
} catch let error {
|
||||
print("Error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func rejectContactRequest(_ contactRequest: UserContactRequest) {
|
||||
do {
|
||||
try apiRejectContactRequest(contactReqId: contactRequest.apiId)
|
||||
chatModel.removeChat(contactRequest.id)
|
||||
} catch let error {
|
||||
print("Error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatListNavLink_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
@State var chatId: String? = "@1"
|
||||
return Group {
|
||||
ChatListNavLink(chat: Chat(
|
||||
chatInfo: sampleDirectChatInfo,
|
||||
chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")]
|
||||
))
|
||||
ChatListNavLink(chat: Chat(
|
||||
chatInfo: sampleDirectChatInfo,
|
||||
chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")]
|
||||
))
|
||||
ChatListNavLink(chat: Chat(
|
||||
chatInfo: sampleContactRequestChatInfo,
|
||||
chatItems: []
|
||||
))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 80))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// ChatListView.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 27/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ChatListView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State private var connectAlert = false
|
||||
@State private var connectError: Error?
|
||||
|
||||
var user: User
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
// if chatModel.chats.isEmpty {
|
||||
// VStack {
|
||||
// Text("Hello chat")
|
||||
// Text("Active user: \(user.localDisplayName) (\(user.profile.fullName))")
|
||||
// }
|
||||
// }
|
||||
|
||||
NavigationView {
|
||||
List {
|
||||
NavigationLink {
|
||||
TerminalView()
|
||||
} label: {
|
||||
Text("Terminal")
|
||||
}
|
||||
|
||||
ForEach(chatModel.chats) { chat in
|
||||
ChatListNavLink(chat: chat)
|
||||
}
|
||||
}
|
||||
.padding(0)
|
||||
.offset(x: -8)
|
||||
.listStyle(.plain)
|
||||
.navigationTitle("Your chats")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
SettingsButton()
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
NewChatButton()
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $connectAlert) { connectionErrorAlert() }
|
||||
}
|
||||
.alert(isPresented: $chatModel.connectViaUrl) { connectViaUrlAlert() }
|
||||
}
|
||||
}
|
||||
|
||||
private func connectViaUrlAlert() -> Alert {
|
||||
if let url = chatModel.appOpenUrl {
|
||||
var path = url.path
|
||||
if (path == "/contact" || path == "/invitation") {
|
||||
path.removeFirst()
|
||||
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
|
||||
return Alert(
|
||||
title: Text("Connect via \(path) link?"),
|
||||
message: Text("Your profile will be sent to the contact that you received this link from: \(link)"),
|
||||
primaryButton: .default(Text("Connect")) {
|
||||
do {
|
||||
try apiConnect(connReq: link)
|
||||
} catch {
|
||||
connectAlert = true
|
||||
connectError = error
|
||||
print(error)
|
||||
}
|
||||
chatModel.appOpenUrl = nil
|
||||
}, secondaryButton: .cancel() {
|
||||
chatModel.appOpenUrl = nil
|
||||
}
|
||||
)
|
||||
} else {
|
||||
return Alert(title: Text("Error: URL not available"))
|
||||
}
|
||||
} else {
|
||||
return Alert(title: Text("Error: URL not available"))
|
||||
}
|
||||
}
|
||||
|
||||
private func connectionErrorAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Connection error"),
|
||||
message: Text(connectError?.localizedDescription ?? "")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
chatModel.chats = [
|
||||
Chat(
|
||||
chatInfo: sampleDirectChatInfo,
|
||||
chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")]
|
||||
),
|
||||
Chat(
|
||||
chatInfo: sampleGroupChatInfo,
|
||||
chatItems: [chatItemSample(1, .directSnd, Date.now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")]
|
||||
),
|
||||
Chat(
|
||||
chatInfo: sampleContactRequestChatInfo,
|
||||
chatItems: []
|
||||
)
|
||||
|
||||
]
|
||||
return ChatListView(user: sampleUser)
|
||||
.environmentObject(chatModel)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// ChatNavLabel.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 28/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ChatPreviewView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
|
||||
var body: some View {
|
||||
let cItem = chat.chatItems.last
|
||||
return VStack(spacing: 4) {
|
||||
HStack(alignment: .top) {
|
||||
Text(chat.chatInfo.localDisplayName)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.padding(.leading, 8)
|
||||
.padding(.top, 4)
|
||||
.frame(maxHeight: .infinity, alignment: .topLeading)
|
||||
Spacer()
|
||||
if let cItem = cItem {
|
||||
Text(getDateFormatter().string(from: cItem.meta.itemTs))
|
||||
.font(.subheadline)
|
||||
.padding(.trailing, 8)
|
||||
.padding(.top, 4)
|
||||
.frame(minWidth: 60, alignment: .trailing)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
if let cItem = cItem {
|
||||
Text(cItem.content.text)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading)
|
||||
.padding([.leading, .trailing], 8)
|
||||
.padding(.bottom, 4)
|
||||
.padding(.top, 1)
|
||||
}
|
||||
// else if case let .direct(contact) = chatPreview.chatInfo, !contact.connected {
|
||||
// Text("Connecting...")
|
||||
// .frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading)
|
||||
// .padding([.leading, .trailing], 8)
|
||||
// .padding(.bottom, 4)
|
||||
// .padding(.top, 1)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatPreviewView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group{
|
||||
ChatPreviewView(chat: Chat(
|
||||
chatInfo: sampleDirectChatInfo,
|
||||
chatItems: []
|
||||
))
|
||||
ChatPreviewView(chat: Chat(
|
||||
chatInfo: sampleDirectChatInfo,
|
||||
chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")]
|
||||
))
|
||||
ChatPreviewView(chat: Chat(
|
||||
chatInfo: sampleGroupChatInfo,
|
||||
chatItems: [chatItemSample(1, .directSnd, Date.now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")]
|
||||
))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 80))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// ContactRequestView.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 02/02/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContactRequestView: View {
|
||||
var contactRequest: UserContactRequest
|
||||
|
||||
var body: some View {
|
||||
return VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(alignment: .top) {
|
||||
Text("@\(contactRequest.localDisplayName)")
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.blue)
|
||||
.padding(.leading, 8)
|
||||
.padding(.top, 4)
|
||||
.frame(maxHeight: .infinity, alignment: .topLeading)
|
||||
Spacer()
|
||||
Text("12:34")// getDateFormatter().string(from: cItem.meta.itemTs))
|
||||
.font(.subheadline)
|
||||
.padding(.trailing, 28)
|
||||
.padding(.top, 4)
|
||||
.frame(minWidth: 60, alignment: .trailing)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Text("wants to connect to you!")
|
||||
.frame(minHeight: 44, maxHeight: 44, alignment: .topLeading)
|
||||
.padding([.leading, .trailing], 8)
|
||||
.padding(.bottom, 4)
|
||||
.padding(.top, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContactRequestView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContactRequestView(contactRequest: sampleContactRequest)
|
||||
.previewLayout(.fixed(width: 360, height: 80))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// AddContactView.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 29/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreImage.CIFilterBuiltins
|
||||
|
||||
struct AddContactView: View {
|
||||
var connReqInvitation: String
|
||||
@State private var shareInvitation = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Add contact")
|
||||
.font(.title)
|
||||
.padding(.bottom)
|
||||
Text("Show QR code to your contact\nto scan from the app")
|
||||
.font(.title2)
|
||||
.multilineTextAlignment(.center)
|
||||
QRCode(uri: connReqInvitation)
|
||||
.padding()
|
||||
Text("If you can't show QR code, you can share the invitation link via any channel")
|
||||
.font(.subheadline)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal)
|
||||
Button { shareInvitation = true } label: {
|
||||
Label("Share", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
.padding()
|
||||
.shareSheet(isPresented: $shareInvitation, items: [connReqInvitation])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AddContactView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddContactView(connReqInvitation: "https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FFe5ICmvrm4wkrr6X1LTMii-lhBqLeB76%23MCowBQYDK2VuAyEAdhZZsHpuaAk3Hh1q0uNb_6hGTpuwBIrsp2z9U2T0oC0%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAcz6jJk71InuxA0bOX7OUhddfB8Ov7xwQIlIDeXBRZaOntUU4brU5Y3rBzroZBdQJi0FKdtt_D7I%3D%2CMEIwBQYDK2VvAzkA-hDvk1duBi1hlOr08VWSI-Ou4JNNSQjseY69QyKm7Kgg1zZjbpGfyBqSZ2eqys6xtoV4ZtoQUXQ%3D")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// ConnectContactView.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 29/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CodeScanner
|
||||
|
||||
struct ConnectContactView: View {
|
||||
var completed: ((Error?) -> Void)
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Scan QR code")
|
||||
.font(.title)
|
||||
.padding(.bottom)
|
||||
Text("Your chat profile will be sent to your contact.")
|
||||
.font(.title2)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding()
|
||||
ZStack {
|
||||
CodeScannerView(codeTypes: [.qr], completion: processQRCode)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.border(.gray)
|
||||
}
|
||||
.padding(13.0)
|
||||
}
|
||||
}
|
||||
|
||||
func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
||||
switch resp {
|
||||
case let .success(r):
|
||||
do {
|
||||
try apiConnect(connReq: r.string)
|
||||
completed(nil)
|
||||
} catch {
|
||||
print(error)
|
||||
completed(error)
|
||||
}
|
||||
case let .failure(e):
|
||||
print(e)
|
||||
completed(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ConnectContactView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
return ConnectContactView(completed: {_ in })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// CreateGroupView.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 29/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CreateGroupView: View {
|
||||
var body: some View {
|
||||
Text("CreateGroupView")
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateGroupView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CreateGroupView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// NewChatButton.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 31/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct NewChatButton: View {
|
||||
@State private var showAddChat = false
|
||||
@State private var addContact = false
|
||||
@State private var addContactAlert = false
|
||||
@State private var addContactError: Error?
|
||||
@State private var connReqInvitation: String = ""
|
||||
@State private var connectContact = false
|
||||
@State private var connectAlert = false
|
||||
@State private var connectError: Error?
|
||||
@State private var createGroup = false
|
||||
|
||||
var body: some View {
|
||||
Button { showAddChat = true } label: {
|
||||
Image(systemName: "square.and.pencil")
|
||||
}
|
||||
.confirmationDialog("Start new chat", isPresented: $showAddChat, titleVisibility: .visible) {
|
||||
Button("Add contact") { addContactAction() }
|
||||
Button("Scan QR code") { connectContact = true }
|
||||
Button("Create group") { createGroup = true }
|
||||
.disabled(true)
|
||||
}
|
||||
.sheet(isPresented: $addContact, content: {
|
||||
AddContactView(connReqInvitation: connReqInvitation)
|
||||
})
|
||||
.alert(isPresented: $addContactAlert) {
|
||||
connectionError(addContactError)
|
||||
}
|
||||
.sheet(isPresented: $connectContact, content: {
|
||||
connectContactSheet()
|
||||
})
|
||||
.alert(isPresented: $connectAlert) {
|
||||
connectionError(connectError)
|
||||
}
|
||||
.sheet(isPresented: $createGroup, content: { CreateGroupView() })
|
||||
}
|
||||
|
||||
func addContactAction() {
|
||||
do {
|
||||
connReqInvitation = try apiAddContact()
|
||||
addContact = true
|
||||
} catch {
|
||||
addContactAlert = true
|
||||
addContactError = error
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
func connectContactSheet() -> some View {
|
||||
ConnectContactView(completed: { err in
|
||||
connectContact = false
|
||||
if err != nil {
|
||||
connectAlert = true
|
||||
connectError = err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func connectionError(_ error: Error?) -> Alert {
|
||||
Alert(
|
||||
title: Text("Connection error"),
|
||||
message: Text(error?.localizedDescription ?? "")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct NewChatButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NewChatButton()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// QRCode.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 30/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreImage.CIFilterBuiltins
|
||||
|
||||
struct QRCode: View {
|
||||
let uri: String
|
||||
@State private var image: UIImage?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if let image = image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.interpolation(.none)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
generateImage()
|
||||
}
|
||||
}
|
||||
|
||||
private func generateImage() {
|
||||
guard image == nil else { return }
|
||||
|
||||
let context = CIContext()
|
||||
let filter = CIFilter.qrCodeGenerator()
|
||||
filter.message = Data(uri.utf8)
|
||||
|
||||
guard
|
||||
let outputImage = filter.outputImage,
|
||||
let cgImage = context.createCGImage(outputImage, from: outputImage.extent)
|
||||
else { return }
|
||||
|
||||
self.image = UIImage(cgImage: cgImage)
|
||||
}
|
||||
}
|
||||
|
||||
struct QRCode_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
QRCode(uri: "https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FFe5ICmvrm4wkrr6X1LTMii-lhBqLeB76%23MCowBQYDK2VuAyEAdhZZsHpuaAk3Hh1q0uNb_6hGTpuwBIrsp2z9U2T0oC0%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAcz6jJk71InuxA0bOX7OUhddfB8Ov7xwQIlIDeXBRZaOntUU4brU5Y3rBzroZBdQJi0FKdtt_D7I%3D%2CMEIwBQYDK2VvAzkA-hDvk1duBi1hlOr08VWSI-Ou4JNNSQjseY69QyKm7Kgg1zZjbpGfyBqSZ2eqys6xtoV4ZtoQUXQ%3D")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// ShareSheet.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 30/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension UIApplication {
|
||||
static let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first
|
||||
static let keyWindowScene = shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene
|
||||
}
|
||||
|
||||
extension View {
|
||||
func shareSheet(isPresented: Binding<Bool>, items: [Any]) -> some View {
|
||||
guard isPresented.wrappedValue else { return self }
|
||||
let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
|
||||
let presentedViewController = UIApplication.keyWindow?.rootViewController?.presentedViewController ?? UIApplication.keyWindow?.rootViewController
|
||||
activityViewController.completionWithItemsHandler = { _, _, _, _ in isPresented.wrappedValue = false }
|
||||
presentedViewController?.present(activityViewController, animated: true)
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
struct ShareSheetTest: View {
|
||||
@State private var isPresentingShareSheet = false
|
||||
|
||||
var body: some View {
|
||||
Button("Show Share Sheet") { isPresentingShareSheet = true }
|
||||
.shareSheet(isPresented: $isPresentingShareSheet, items: ["Share me!"])
|
||||
}
|
||||
}
|
||||
|
||||
struct ShareSheetTest_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ShareSheetTest()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// TerminalView.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 27/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TerminalView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State var inProgress: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(chatModel.terminalItems) { item in
|
||||
NavigationLink {
|
||||
ScrollView {
|
||||
Text(item.details)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
} label: {
|
||||
Text(item.label)
|
||||
.frame(width: 360, height: 30, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
|
||||
Spacer()
|
||||
|
||||
SendMessageView(sendMessage: sendMessage, inProgress: inProgress)
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(_ cmdStr: String) {
|
||||
let cmd = ChatCommand.string(cmdStr)
|
||||
chatModel.terminalItems.append(.cmd(Date.now, cmd))
|
||||
|
||||
DispatchQueue.global().async {
|
||||
inProgress = true
|
||||
do {
|
||||
let r = try chatSendCmd(cmd)
|
||||
DispatchQueue.main.async {
|
||||
chatModel.terminalItems.append(.resp(Date.now, r))
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
inProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TerminalView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
chatModel.terminalItems = [
|
||||
.resp(Date.now, ChatResponse.response(type: "contactSubscribed", json: "{}")),
|
||||
.resp(Date.now, ChatResponse.response(type: "newChatItem", json: "{}"))
|
||||
]
|
||||
return NavigationView {
|
||||
TerminalView()
|
||||
.environmentObject(chatModel)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// SettingsButton.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 31/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsButton: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State private var showSettings = false
|
||||
|
||||
var body: some View {
|
||||
Button { showSettings = true } label: {
|
||||
Image(systemName: "gearshape")
|
||||
}
|
||||
.sheet(isPresented: $showSettings, content: {
|
||||
SettingsView()
|
||||
.onAppear {
|
||||
do {
|
||||
chatModel.userAddress = try apiGetUserAddress()
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsButton()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// SettingsView.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 31/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
|
||||
var body: some View {
|
||||
UserProfile()
|
||||
UserAddress()
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
chatModel.currentUser = sampleUser
|
||||
return SettingsView()
|
||||
.environmentObject(chatModel)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// UserAddress.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 31/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct UserAddress: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State private var shareAddressLink = false
|
||||
@State private var deleteAddressAlert = false
|
||||
|
||||
var body: some View {
|
||||
VStack (alignment: .leading) {
|
||||
Text("Your chat address")
|
||||
.font(.title)
|
||||
.padding(.bottom)
|
||||
Text("Your can share your address as a link or as a QR code - anybody will be able to connect to you, and if you later delete it - you won't lose your contacts.")
|
||||
.padding(.bottom)
|
||||
if let userAdress = chatModel.userAddress {
|
||||
QRCode(uri: userAdress)
|
||||
HStack {
|
||||
Button { shareAddressLink = true } label: {
|
||||
Label("Share link", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
.padding()
|
||||
.shareSheet(isPresented: $shareAddressLink, items: [userAdress])
|
||||
|
||||
Button { deleteAddressAlert = true } label: {
|
||||
Label("Delete address", systemImage: "trash")
|
||||
}
|
||||
.padding()
|
||||
.alert(isPresented: $deleteAddressAlert) {
|
||||
Alert(
|
||||
title: Text("Delete address?"),
|
||||
message: Text("All your contacts will remain connected"),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
do {
|
||||
try apiDeleteUserAddress()
|
||||
chatModel.userAddress = nil
|
||||
} catch let error {
|
||||
print("Error: \(error)")
|
||||
}
|
||||
}, secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
.shareSheet(isPresented: $shareAddressLink, items: [userAdress])
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
Button {
|
||||
do {
|
||||
chatModel.userAddress = try apiCreateUserAddress()
|
||||
} catch let error {
|
||||
print("Error: \(error)")
|
||||
}
|
||||
} label: { Label("Create address", systemImage: "qrcode") }
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct UserAddress_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
chatModel.userAddress = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"
|
||||
return UserAddress()
|
||||
.environmentObject(chatModel)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// UserProfile.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 31/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct UserProfile: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State private var profile = Profile(displayName: "", fullName: "")
|
||||
@State private var editProfile: Bool = false
|
||||
|
||||
var body: some View {
|
||||
let user: User = chatModel.currentUser!
|
||||
|
||||
return VStack(alignment: .leading) {
|
||||
Text("Your chat profile")
|
||||
.font(.title)
|
||||
.padding(.bottom)
|
||||
Text("Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile.")
|
||||
.padding(.bottom)
|
||||
if editProfile {
|
||||
VStack(alignment: .leading) {
|
||||
TextField("Display name", text: $profile.displayName)
|
||||
.textInputAutocapitalization(.never)
|
||||
.disableAutocorrection(true)
|
||||
.padding(.bottom)
|
||||
TextField("Full name (optional)", text: $profile.fullName)
|
||||
.textInputAutocapitalization(.never)
|
||||
.disableAutocorrection(true)
|
||||
.padding(.bottom)
|
||||
HStack(spacing: 20) {
|
||||
Button("Cancel") { editProfile = false }
|
||||
Button("Save (and notify contacts)") { saveProfile() }
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("Display name:")
|
||||
Text(user.profile.displayName)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
.padding(.bottom)
|
||||
HStack {
|
||||
Text("Full name:")
|
||||
Text(user.profile.fullName)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
.padding(.bottom)
|
||||
Button("Edit") {
|
||||
profile = user.profile
|
||||
editProfile = true
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
func saveProfile() {
|
||||
do {
|
||||
if let newProfile = try apiUpdateProfile(profile: profile) {
|
||||
chatModel.currentUser?.profile = newProfile
|
||||
profile = newProfile
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
editProfile = false
|
||||
}
|
||||
}
|
||||
|
||||
struct UserProfile_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
chatModel.currentUser = sampleUser
|
||||
return UserProfile()
|
||||
.environmentObject(chatModel)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// WelcomeView.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 18/01/2022.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct WelcomeView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State var displayName: String = ""
|
||||
@State var fullName: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Create profile")
|
||||
.font(.largeTitle)
|
||||
.padding(.bottom)
|
||||
Text("Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile.")
|
||||
.padding(.bottom)
|
||||
TextField("Display name", text: $displayName)
|
||||
.textInputAutocapitalization(.never)
|
||||
.disableAutocorrection(true)
|
||||
.padding(.bottom)
|
||||
TextField("Full name (optional)", text: $fullName)
|
||||
.textInputAutocapitalization(.never)
|
||||
.disableAutocorrection(true)
|
||||
.padding(.bottom)
|
||||
Button("Create") {
|
||||
let profile = Profile(
|
||||
displayName: displayName,
|
||||
fullName: fullName
|
||||
)
|
||||
if let user = chatCreateUser(profile) {
|
||||
chatModel.currentUser = user
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct WelcomeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
WelcomeView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//
|
||||
// dummy.m
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 22/01/2022.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:simplex.chat</string>
|
||||
<string>applinks:www.simplex.chat</string>
|
||||
<string>applinks:simplex.chat?mode=developer</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>chat.simplex.app</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>simplex</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "CodeScanner",
|
||||
"repositoryURL": "https://github.com/twostraws/CodeScanner",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "c27a66149b7483fe42e2ec6aad61d5c3fffe522d",
|
||||
"version": "2.1.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Tests_iOS.swift
|
||||
// Tests iOS
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 17/01/2022.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class Tests_iOS: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
|
||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||
continueAfterFailure = false
|
||||
|
||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// UI tests must launch the application that they test.
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Use recording to get started writing UI tests.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
func testLaunchPerformance() throws {
|
||||
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
|
||||
// This measures how long it takes to launch your application.
|
||||
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||
XCUIApplication().launch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Tests_iOSLaunchTests.swift
|
||||
// Tests iOS
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 17/01/2022.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class Tests_iOSLaunchTests: XCTestCase {
|
||||
|
||||
override class var runsForEachTargetApplicationUIConfiguration: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
}
|
||||
|
||||
func testLaunch() throws {
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Insert steps here to perform after app launch but before taking a screenshot,
|
||||
// such as logging into a test account or navigating somewhere in the app
|
||||
|
||||
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||
attachment.name = "Launch Screen"
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Tests_macOS.swift
|
||||
// Tests macOS
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 17/01/2022.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class Tests_macOS: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
|
||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||
continueAfterFailure = false
|
||||
|
||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// UI tests must launch the application that they test.
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Use recording to get started writing UI tests.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
func testLaunchPerformance() throws {
|
||||
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
|
||||
// This measures how long it takes to launch your application.
|
||||
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||
XCUIApplication().launch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Tests_macOSLaunchTests.swift
|
||||
// Tests macOS
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 17/01/2022.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class Tests_macOSLaunchTests: XCTestCase {
|
||||
|
||||
override class var runsForEachTargetApplicationUIConfiguration: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
}
|
||||
|
||||
func testLaunch() throws {
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Insert steps here to perform after app launch but before taking a screenshot,
|
||||
// such as logging into a test account or navigating somewhere in the app
|
||||
|
||||
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||
attachment.name = "Launch Screen"
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,119 +0,0 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Demo where
|
||||
|
||||
import Simplex.Chat.Styled
|
||||
import System.Console.ANSI.Types
|
||||
import System.Terminal
|
||||
|
||||
someViewUpdate :: Monad m => m ()
|
||||
someViewUpdate = pure ()
|
||||
|
||||
chatLayoutDemo :: MonadTerminal m => m ()
|
||||
chatLayoutDemo =
|
||||
mapM_
|
||||
putStyledLn
|
||||
[ " search " <> Styled gray "(ctrl-s) " <> lineV <> Styled toContact " @bob " <> "Bob Roberts " <> Styled greenColor "@john" <> "",
|
||||
" " <> lineV <> Styled gray " 14:15 online profile (ctrl-p)",
|
||||
lineH 20 <> crossover <> lineH 59,
|
||||
"* " <> Styled [SetConsoleIntensity BoldIntensity] "all chats " <> " " <> lineV <> "",
|
||||
Styled gray " (ctrl-a) " <> lineV <> "",
|
||||
"*" <> Styled toContact " @alice " <> Styled darkGray "14:37 " <> lineV <> "",
|
||||
Styled gray " Hello there! ... " <> lineV <> "",
|
||||
Styled selected " " <> Styled (toContact <> selected) " @bob " <> Styled (selected <> gray) "12:35 " <> lineV <> "",
|
||||
Styled selected " All good, John... " <> lineV <> "",
|
||||
"*" <> Styled group " #team " <> Styled darkGray "10:55 " <> lineV <> "",
|
||||
Styled gray " What's up ther... " <> lineV <> "",
|
||||
" " <> Styled toContact " @tom " <> Styled darkGray "Wed " <> lineV <> "",
|
||||
Styled gray " Have you seen ... " <> lineV <> "",
|
||||
" " <> lineV,
|
||||
" " <> lineV,
|
||||
" " <> lineV,
|
||||
" " <> lineV,
|
||||
" " <> lineV,
|
||||
" " <> lineV <> Styled greenColor " ✔︎" <> Styled darkGray " 12:30" <> Styled toContact " @bob" <> " hey bob - how is it going?",
|
||||
" " <> lineV <> Styled greenColor " ✔︎" <> Styled darkGray " " <> Styled toContact " " <> " let's meet soon!",
|
||||
" " <> lineV <> " *" <> Styled darkGray " 12:35" <> Styled contact " bob>" <> " All good, John! How are you?",
|
||||
" " <> teeL <> lineH 59,
|
||||
" " <> lineV <> " > " <> Styled toContact "@bob" <> " 😀 This is the message that will be sent to @bob"
|
||||
]
|
||||
>> putStyled (Styled ctrlKeys " help (ctrl-h) new contact (ctrl-n) choose chat (ctrl-↓↑) new group (ctrl-g) ")
|
||||
|
||||
contact :: [SGR]
|
||||
contact = [SetConsoleIntensity BoldIntensity, SetColor Foreground Vivid Yellow]
|
||||
|
||||
toContact :: [SGR]
|
||||
toContact = [SetConsoleIntensity BoldIntensity, SetColor Foreground Vivid Cyan]
|
||||
|
||||
group :: [SGR]
|
||||
group = [SetConsoleIntensity BoldIntensity, SetColor Foreground Vivid Cyan]
|
||||
|
||||
selected :: [SGR]
|
||||
selected = [SetColor Background Vivid Black]
|
||||
|
||||
ctrlKeys :: [SGR]
|
||||
ctrlKeys = [SetColor Background Dull White, SetColor Foreground Dull Black]
|
||||
|
||||
gray :: [SGR]
|
||||
gray = [SetColor Foreground Dull White]
|
||||
|
||||
darkGray :: [SGR]
|
||||
darkGray = [SetColor Foreground Vivid Black]
|
||||
|
||||
greenColor :: [SGR]
|
||||
greenColor = [SetColor Foreground Vivid Green]
|
||||
|
||||
lineV :: StyledString
|
||||
lineV = Styled selected " " -- "\x2502"
|
||||
|
||||
lineH :: Int -> StyledString
|
||||
lineH n = Styled darkGray $ replicate n '\x2500'
|
||||
|
||||
teeL :: StyledString
|
||||
teeL = Styled selected " " -- "\x251C"
|
||||
|
||||
crossover :: StyledString
|
||||
crossover = Styled selected " " -- "\x253C"
|
||||
|
||||
putStyledLn :: MonadTerminal m => StyledString -> m ()
|
||||
putStyledLn s = putStyled s >> putLn
|
||||
|
||||
putStyled :: MonadTerminal m => StyledString -> m ()
|
||||
putStyled (s1 :<>: s2) = putStyled s1 >> putStyled s2
|
||||
putStyled (Styled [] s) = putString s
|
||||
putStyled (Styled sgr s) = setSGR sgr >> putString s >> resetAttributes
|
||||
|
||||
setSGR :: MonadTerminal m => [SGR] -> m ()
|
||||
setSGR = mapM_ $ \case
|
||||
Reset -> resetAttributes
|
||||
SetConsoleIntensity BoldIntensity -> setAttribute bold
|
||||
SetConsoleIntensity _ -> resetAttribute bold
|
||||
SetItalicized True -> setAttribute italic
|
||||
SetItalicized _ -> resetAttribute italic
|
||||
SetUnderlining NoUnderline -> resetAttribute underlined
|
||||
SetUnderlining _ -> setAttribute underlined
|
||||
SetSwapForegroundBackground True -> setAttribute inverted
|
||||
SetSwapForegroundBackground _ -> resetAttribute inverted
|
||||
SetColor l i c -> setAttribute . layer l . intensity i $ color c
|
||||
SetBlinkSpeed _ -> pure ()
|
||||
SetVisible _ -> pure ()
|
||||
SetRGBColor _ _ -> pure ()
|
||||
SetPaletteColor _ _ -> pure ()
|
||||
SetDefaultColor _ -> pure ()
|
||||
where
|
||||
layer = \case
|
||||
Foreground -> foreground
|
||||
Background -> background
|
||||
intensity = \case
|
||||
Dull -> id
|
||||
Vivid -> bright
|
||||
color = \case
|
||||
Black -> black
|
||||
Red -> red
|
||||
Green -> green
|
||||
Yellow -> yellow
|
||||
Blue -> blue
|
||||
Magenta -> magenta
|
||||
Cyan -> cyan
|
||||
White -> white
|
||||
@@ -6,7 +6,9 @@
|
||||
module Main where
|
||||
|
||||
import Simplex.Chat
|
||||
import Simplex.Chat.Controller (versionNumber)
|
||||
import Simplex.Chat.Options
|
||||
import Simplex.Chat.Terminal
|
||||
import System.Directory (getAppUserDataDirectory)
|
||||
import System.Terminal (withTerminal)
|
||||
|
||||
@@ -19,40 +21,8 @@ main = do
|
||||
welcomeGetOpts :: IO ChatOpts
|
||||
welcomeGetOpts = do
|
||||
appDir <- getAppUserDataDirectory "simplex"
|
||||
opts@ChatOpts {dbFile} <- getChatOpts appDir
|
||||
putStrLn "SimpleX chat prototype v0.4.0"
|
||||
putStrLn $ "db: " <> dbFile <> ".chat.db, " <> dbFile <> ".agent.db"
|
||||
opts@ChatOpts {dbFilePrefix} <- getChatOpts appDir
|
||||
putStrLn $ "SimpleX Chat v" ++ versionNumber
|
||||
putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db"
|
||||
putStrLn "type \"/help\" or \"/h\" for usage info"
|
||||
pure opts
|
||||
|
||||
-- defaultSettings :: C.Size -> C.VirtualTerminalSettings
|
||||
-- defaultSettings size =
|
||||
-- C.VirtualTerminalSettings
|
||||
-- { C.virtualType = "xterm",
|
||||
-- C.virtualWindowSize = pure size,
|
||||
-- C.virtualEvent = retry,
|
||||
-- C.virtualInterrupt = retry
|
||||
-- }
|
||||
|
||||
-- main :: IO ()
|
||||
-- main = do
|
||||
-- void $ createStore "simplex-chat.db" 4
|
||||
|
||||
-- hFlush stdout
|
||||
-- -- ChatTerminal {termSize} <- newChatTerminal
|
||||
-- -- pos <- C.withVirtualTerminal (defaultSettings termSize) $
|
||||
-- -- \t -> runTerminalT (C.setAlternateScreenBuffer True >> C.putString "a" >> C.flush >> C.getCursorPosition) t
|
||||
-- -- print pos
|
||||
-- -- race_ (printEvents t) (updateTerminal t)
|
||||
-- void . withTerminal . runTerminalT $ chatLayoutDemo >> C.flush >> C.awaitEvent
|
||||
|
||||
-- printEvents :: C.VirtualTerminal -> IO ()
|
||||
-- printEvents t = forever $ do
|
||||
-- event <- withTerminal . runTerminalT $ C.flush >> C.awaitEvent
|
||||
-- runTerminalT (putStringLn $ show event) t
|
||||
|
||||
-- updateTerminal :: C.VirtualTerminal -> IO ()
|
||||
-- updateTerminal t = forever $ do
|
||||
-- threadDelay 10000
|
||||
-- win <- readTVarIO $ C.virtualWindow t
|
||||
-- withTerminal . runTerminalT $ mapM_ C.putStringLn win >> C.flush
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Simplex chat
|
||||
|
||||
**Published:** Oct 22, 2020
|
||||
|
||||
https://simplex.chat
|
||||
|
||||
I'd really appreciate your feedback, criticism and suggestions on the open-source idea I was slowly working on since early 2020. I recently made the demo server for the low-level message queue protocol ("simplex messaging protocol") and the website to try to explain the chat idea that would use this protocol.
|
||||
|
||||
Haskell protocol implementation: https://github.com/simplex-chat/simplexmq
|
||||
|
||||
In short, the protocol defines a minimalist set of commands and server responses (just 7 commands and 5 responses sent over TCP) to operate encrypted message queues with in-memory persistence - the implementation uses STM.
|
||||
|
||||
If anything, it was definitely helping to get to know Haskell types etc. much deeper than before :)
|
||||
|
||||
Any criticism would be great - thank you in advance!
|
||||
|
||||
---
|
||||
|
||||
Originally published at https://www.reddit.com/r/haskell/comments/jg6uh4/simplex_chat/
|
||||
@@ -0,0 +1,25 @@
|
||||
## Announcing SimpleX Chat Prototype!
|
||||
|
||||
**Published:** May 12, 2021
|
||||
|
||||
For the last six months [me](https://github.com/epoberezkin) and my son [Efim](https://github.com/efim-poberezkin) have been working to bring you a working prototype of SimpleX Chat. We're excited to announce SimpleX Chat terminal client is now available [here](https://github.com/simplex-chat/simplex-chat) on Linux, Windows and Mac (you can either build from source or download the binary for Linux, Windows or Mac from the latest release).
|
||||
|
||||
We’ve been using the terminal client between us and a few other people for a couple of months now, eating our own “dog food”, and have developed up to version 0.3.1, with most of the messaging protocol features we originally planned
|
||||
|
||||
### Features
|
||||
|
||||
- End-to-end encryption with protection from man in the middle attack. The connection invitation must be passed out-of-band (see [how to use SimpleX Chat](https://github.com/simplex-chat/simplex-chat#how-to-use-simplex-chat) in the repo).
|
||||
- No global identity or any usernames visible to the server(s), ensuring full privacy of your contacts and conversations.
|
||||
- Message signing and verification with automatically generated RSA keys, with keys being unique per each connection.
|
||||
- Authorization of each command/message by the servers with automatically generated RSA key pairs, also unique per connection.
|
||||
- Message integrity validation (via passing the digests of the previous messages).
|
||||
- Encrypted TCP transport, independent of certificates.
|
||||
- You can deploy your own server, but you don’t have to - the demo SMP server to relay your messages is available at smp1.simplex.im:5223 (pre-configured in the client).
|
||||
|
||||
### We need your help!
|
||||
|
||||
We're building a new kind of chat network - the only network that lets you control your chat. We'd really appreciate your feedback, criticism and support - a star on the github repo, signing up to the mailing list or any contribution to the project will help. There is so much more to do!
|
||||
|
||||
---
|
||||
|
||||
Originally published at https://www.reddit.com/r/haskell/comments/naw6lz/simplex_chat_prototype_terminal_ui_made_in_haskell/
|
||||