Merge branch 'mp-gallery' into hn/android-integrate-with-media-api

This commit is contained in:
Evgeny Poberezkin
2026-01-20 18:33:25 +00:00
31 changed files with 304 additions and 114 deletions

View File

@@ -605,6 +605,9 @@ jobs:
- name: Checkout current repository
uses: actions/checkout@v6
- name: Install packages for archiving
run: sudo apt install -y msitools gcc-mingw-w64
- name: Build archives
run: |
INIT_DIR='${{ runner.temp }}/artifacts'
@@ -612,6 +615,8 @@ jobs:
TAG='${{ github.ref_name }}'
URL='https://github.com/${{ github.repository }}/releases/download'
PREFIX='${{ github.event.repository.name }}-libs'
# Windows-specific
FILE_URL='https://raw.githubusercontent.com/${{ github.repository }}/refs/tags/${{ github.ref_name }}'
# Setup directories
mkdir "$INIT_DIR" "$RELEASE_DIR" && cd "$INIT_DIR"
@@ -620,6 +625,7 @@ jobs:
curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-ubuntu-22_04-x86_64.deb" -o linux.deb
curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-macos-aarch64.dmg" -o macos-aarch64.dmg
curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-macos-x86_64.dmg" -o macos-x86_64.dmg
curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-windows-x86_64.msi" -o windows-x86_64.msi
# Linux
# -----
@@ -646,6 +652,18 @@ jobs:
zip -r "${PREFIX}-macos-x86_64.zip" libs
mv "${PREFIX}-macos-x86_64.zip" "$RELEASE_DIR" && cd "$INIT_DIR"
# Windows: x86_64
# ---------------
msiextract windows-x86_64.msi -C windows-out && cd windows-out/SimpleX/app/resources
# We need to generate library that exports symbols from Windows dll
curl --proto '=https' --tlsv1.2 -sSf -LO "${FILE_URL}/libsimplex.dll.def"
x86_64-w64-mingw32-dlltool -d libsimplex.dll.def -l libsimplex.lib -D libsimplex.dll
mkdir libs && cp *.dll *.lib libs/
zip -r "${PREFIX}-windows-x86_64.zip" libs
mv "${PREFIX}-windows-x86_64.zip" "$RELEASE_DIR" && cd "$INIT_DIR"
- name: Create release in libs repo and upload artifacts
uses: softprops/action-gh-release@v2
with:

View File

@@ -42,7 +42,7 @@
## Connect to the team
You can connect to the team via the app using "chat with the developers button" available when you have no conversations in the profile, "Send questions and ideas" in the app settings or via our [SimpleX address](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). Please connect to:
You can connect to the team via the app using "chat with the developers button" available when you have no conversations in the profile, "Send questions and ideas" in the app settings or via our [SimpleX address](https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw). Please connect to:
- to ask any questions
- to suggest any improvements
@@ -54,7 +54,7 @@ If you are interested in helping us to integrate open-source language models, an
## Join user groups
You can find the groups created by users in [SimpleX Directory](https://simplex.chat/directory/). It is also available as [SimpleX bot](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) that allows to add your own groups and communities to the directory. We are not responsible for the content shared in these groups.
You can find the groups created by users in [SimpleX Directory](https://simplex.chat/directory/). It is also available as [SimpleX bot](https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok) that allows to add your own groups and communities to the directory. We are not responsible for the content shared in these groups.
**Please note**: The groups below are created for the users to be able to ask questions, make suggestions and ask questions about SimpleX Chat only.

View File

@@ -94,6 +94,5 @@ mkChatOpts BroadcastBotOpts {coreOptions, botDisplayName} =
autoAcceptFileSize = 0,
muteNotifications = True,
markRead = False,
createBot = Just CreateBotOpts {botDisplayName, allowFiles = False},
maintenance = False
createBot = Just CreateBotOpts {botDisplayName, allowFiles = False}
}

View File

@@ -27,6 +27,7 @@ data DirectoryOpts = DirectoryOpts
adminUsers :: [KnownContact],
superUsers :: [KnownContact],
ownersGroup :: Maybe KnownGroup,
noAddress :: Bool, -- skip creating address
blockedWordsFile :: Maybe FilePath,
blockedFragmentsFile :: Maybe FilePath,
blockedExtensionRules :: Maybe FilePath,
@@ -70,6 +71,11 @@ directoryOpts appDir defaultDbName = do
<> metavar "OWNERS_GROUP"
<> help "The group of group owners in the format GROUP_ID:DISPLAY_NAME - owners of listed groups will be invited automatically"
)
noAddress <-
switch
( long "no-address"
<> help "skip checking and creating service address"
)
blockedWordsFile <-
optional $
strOption
@@ -153,6 +159,7 @@ directoryOpts appDir defaultDbName = do
adminUsers,
superUsers,
ownersGroup,
noAddress,
blockedWordsFile,
blockedFragmentsFile,
blockedExtensionRules,
@@ -194,8 +201,7 @@ mkChatOpts DirectoryOpts {coreOptions, serviceName} =
autoAcceptFileSize = 0,
muteNotifications = True,
markRead = False,
createBot = Just CreateBotOpts {botDisplayName = serviceName, allowFiles = False},
maintenance = False
createBot = Just CreateBotOpts {botDisplayName = serviceName, allowFiles = False}
}
parseMigrateLog :: ReadM MigrateLog

View File

@@ -184,10 +184,11 @@ directoryPreStartHook :: DirectoryOpts -> ChatController -> IO ()
directoryPreStartHook opts ChatController {config, chatStore} = runDirectoryMigrations opts config chatStore
directoryPostStartHook :: DirectoryOpts -> ServiceState -> ChatController -> IO ()
directoryPostStartHook opts env cc =
directoryPostStartHook opts@DirectoryOpts {noAddress, testing} env cc =
readTVarIO (currentUser cc) >>= \case
Nothing -> putStrLn "No current user" >> exitFailure
Just User {userId, profile = p@LocalProfile {preferences}} -> do
unless noAddress $ initializeBotAddress' (not testing) cc
listingsUpdated env cc
let cmds = fromMaybe [] $ preferences >>= commands_
unless (cmds == directoryCommands) $ do
@@ -216,7 +217,7 @@ directoryCommands =
idParam = Just "<ID>"
directoryService :: DirectoryLog -> DirectoryOpts -> ChatConfig -> IO ()
directoryService st opts@DirectoryOpts {testing} cfg = do
directoryService st opts cfg = do
env <- newServiceState opts
let chatHooks =
defaultChatHooks
@@ -224,8 +225,7 @@ directoryService st opts@DirectoryOpts {testing} cfg = do
postStartHook = Just $ directoryPostStartHook opts env,
acceptMember = Just $ acceptMemberHook opts env
}
simplexChatCore cfg {chatHooks} (mkChatOpts opts) $ \user cc -> do
initializeBotAddress' (not testing) cc
simplexChatCore cfg {chatHooks} (mkChatOpts opts) $ \user cc ->
raceAny_ $
[ forever $ void getLine,
forever $ do

View File

@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 58212c421aa6abb6ad894b8231d8a380849b704b
tag: ca26c69937083deee43b8b2200ec9ef4c004ceac
source-repository-package
type: git

View File

@@ -14,7 +14,7 @@ Please share your use cases and implementations.
## Quick start: a simple bot
```
npm i simplex-chat@6.5.0-beta.4.2
npm i simplex-chat@6.5.0-beta.4.4
```
Simple bot that replies with squares of numbers you send to it:
@@ -28,7 +28,7 @@ Simple bot that replies with squares of numbers you send to it:
profile: {displayName: "Squaring bot example", fullName: ""},
dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""},
options: {
addressSettings: {welcomeMessage: "If you send me a number, I will calculate its square."},
addressSettings: {welcomeMessage: "Send a number, I will square it.",
},
onMessage: async (ci, content) => {
const n = +content.text
@@ -44,9 +44,11 @@ Simple bot that replies with squares of numbers you send to it:
If you installed this package as dependency, you can run this example with:
```sh
node ./node_modules/simplex-chat/examples/squaring-bot-readme.js`
node ./node_modules/simplex-chat/examples/squaring-bot-readme.js
```
If you run it on Mac, the first time it will take 20-30 seconds for MacOS to verify the library.
If you cloned this repository, you can:
```

View File

@@ -9,15 +9,15 @@
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
],
"libraries": [
"-L<(module_root_dir)/libs",
"-lsimplex"
],
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ],
"conditions": [
["OS=='mac'", {
"libraries": [
"-L<(module_root_dir)/libs",
"-lsimplex"
],
"xcode_settings": {
"OTHER_LDFLAGS": [
"-Wl,-rpath,@loader_path/../../libs"
@@ -25,9 +25,24 @@
}
}],
["OS=='linux'", {
"libraries": [
"-L<(module_root_dir)/libs",
"-lsimplex"
],
"ldflags": [
"-Wl,-rpath,'$$ORIGIN'/../../libs"
]
}],
["OS=='win'", {
"libraries": [
"<(module_root_dir)/libs/libsimplex.lib"
],
"copies": [{
"destination": "<(PRODUCT_DIR)",
"files": [
"<(module_root_dir)/libs/*"
]
}]
}]
]
}

View File

@@ -10,6 +10,17 @@ namespace simplex {
using namespace Napi;
void haskell_init() {
#ifdef _WIN32
// non-moving GC is broken on windows with GHC 9.4-9.6.3
int argc = 5;
const char *argv[] = {
"simplex",
"+RTS", // requires `hs_init_with_rtsopts`
"-A64m", // chunk size for new allocations
"-H64m", // initial heap size
"--install-signal-handlers=no",
nullptr};
#else
int argc = 6;
const char *argv[] = {
"simplex",
@@ -19,6 +30,7 @@ void haskell_init() {
"-xn", // non-moving GC
"--install-signal-handlers=no",
nullptr};
#endif
char **pargv = const_cast<char **>(argv);
hs_init_with_rtsopts(&argc, &pargv);
}

View File

@@ -4,7 +4,7 @@
profile: {displayName: "Squaring bot example", fullName: ""},
dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""},
options: {
addressSettings: {welcomeMessage: "If you send me a number, I will calculate its square."},
addressSettings: {welcomeMessage: "Send a number, I will square it."},
},
onMessage: async (ci, content) => {
const n = +content.text

View File

@@ -1,6 +1,6 @@
{
"name": "simplex-chat",
"version": "6.5.0-beta.4.2",
"version": "6.5.0-beta.4.4",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
set -e

View File

@@ -3,7 +3,7 @@
set -e
OS=mac
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
ARCH="${1:-$(uname -m)}"
COMPOSE_ARCH=$ARCH
GHC_VERSION=9.6.3
DATABASE_BACKEND="${2:-sqlite}"

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
set -e

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
set -e

View File

@@ -2,7 +2,7 @@
set -e
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
ARCH="${1:-$(uname -m)}"
if [ "$ARCH" == "arm64" ]; then
ARCH=aarch64
vlc_arch=arm64

View File

@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."58212c421aa6abb6ad894b8231d8a380849b704b" = "1awgvhqfi7gv3xl10h21a6w2hhqc48pq6yq4f83awg1zxkh3hiqn";
"https://github.com/simplex-chat/simplexmq.git"."ca26c69937083deee43b8b2200ec9ef4c004ceac" = "1p7jhxcbn95kddfwa5rjpzfx78fzic03wmy9dmh1mj3j14vyfn02";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";

View File

@@ -18,6 +18,7 @@ CMDS="curl git docker"
INIT_DIR="$PWD"
TEMPDIR="$(mktemp -d)"
PID_MAX_ORIGINAL="$(sysctl -n kernel.pid_max)"
ARCHES="${ARCHES:-aarch64 armv7a}"
@@ -32,6 +33,15 @@ cleanup() {
rm -rf -- "${TEMPDIR}"
docker rm --force "${CONTAINER_NAME}" 2>/dev/null || :
docker image rm "${IMAGE_NAME}" 2>/dev/null || :
if [ "$(sysctl -n kernel.pid_max)" != "$PID_MAX_ORIGINAL" ]; then
printf 'Adjusting kernel.pid_max back to original value...\n'
if $SUDO sysctl kernel.pid_max="$PID_MAX_ORIGINAL"; then
printf 'Successfully adjusted kernel.pid_max\n'
else
printf 'Failed to adjust kernel.pid_max. Please set the value manually with: %s sysctl kernel.pid_max=%s\n' "$SUDO" "$PID_MAX_ORIGINAL"
fi
fi
}
trap 'cleanup' EXIT INT
@@ -52,6 +62,19 @@ check() {
exit 1
fi
if [ "$PID_MAX_ORIGINAL" -gt 65535 ]; then
SUDO=$(command -v sudo || command -v doas) || { echo "No sudo or doas"; exit 1; }
printf 'Adjusting kernel.pid_max value to 65535...\n'
if $SUDO sysctl kernel.pid_max=65535; then
printf 'Successfully adjusted kernel.pid_max\n'
else
printf 'Failed to adjust kernel.pid_max, aborting.\n'
exit 1
fi
fi
set -u
}
@@ -182,6 +205,9 @@ main() {
3) build core library with nix (12-24 hours).
4) build APK and compare with downloaded one
The script will ask for sudo password to adjust kernel.pid_max (needed for armv7a build)
and set it back to otiginal value when the build is done.
Continue?'
read _

View File

@@ -38,7 +38,7 @@ import Text.Read (readMaybe)
import UnliftIO.Async
simplexChatCore :: ChatConfig -> ChatOpts -> (User -> ChatController -> IO ()) -> IO ()
simplexChatCore cfg@ChatConfig {confirmMigrations, testView, chatHooks} opts@ChatOpts {coreOptions = CoreChatOpts {dbOptions, logAgent, yesToUpMigrations, migrationBackupPath}, createBot, maintenance} chat =
simplexChatCore cfg@ChatConfig {confirmMigrations, testView, chatHooks} opts@ChatOpts {coreOptions = CoreChatOpts {dbOptions, logAgent, yesToUpMigrations, migrationBackupPath, maintenance}, createBot} chat =
case logAgent of
Just level -> do
setLogLevel level
@@ -54,13 +54,16 @@ simplexChatCore cfg@ChatConfig {confirmMigrations, testView, chatHooks} opts@Cha
u_ <- getSelectActiveUser chatStore
let backgroundMode = maintenance
cc <- newChatController db u_ cfg opts backgroundMode
u <- maybe (createActiveUser cc createBot) pure u_
forM_ (preStartHook chatHooks) ($ cc)
u <- maybe (noMaintenance >> createActiveUser cc createBot) pure u_
unless testView $ putStrLn $ "Current user: " <> userStr u
unless maintenance $ forM_ (preStartHook chatHooks) ($ cc)
runSimplexChat opts u cc chat
noMaintenance = when maintenance $ do
putStrLn "exiting: no active user in maintenance mode"
exitFailure
runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController -> IO ()) -> IO ()
runSimplexChat ChatOpts {maintenance} u cc@ChatController {config = ChatConfig {chatHooks}} chat
runSimplexChat ChatOpts {coreOptions = CoreChatOpts {maintenance}} u cc@ChatController {config = ChatConfig {chatHooks}} chat
| maintenance = wait =<< async (chat u cc)
| otherwise = do
a1 <- runReaderT (startChatController True True) cc

View File

@@ -255,7 +255,8 @@ mobileChatOpts dbOptions =
deviceName = Nothing,
highlyAvailable = False,
yesToUpMigrations = False,
migrationBackupPath = Just ""
migrationBackupPath = Just "",
maintenance = True
},
chatCmd = "",
chatCmdDelay = 3,
@@ -268,8 +269,7 @@ mobileChatOpts dbOptions =
autoAcceptFileSize = 0,
muteNotifications = True,
markRead = False,
createBot = Nothing,
maintenance = True
createBot = Nothing
}
defaultMobileConfig :: ChatConfig

View File

@@ -50,8 +50,7 @@ data ChatOpts = ChatOpts
autoAcceptFileSize :: Integer,
muteNotifications :: Bool,
markRead :: Bool,
createBot :: Maybe CreateBotOpts,
maintenance :: Bool
createBot :: Maybe CreateBotOpts
}
data CoreChatOpts = CoreChatOpts
@@ -68,7 +67,8 @@ data CoreChatOpts = CoreChatOpts
deviceName :: Maybe Text,
highlyAvailable :: Bool,
yesToUpMigrations :: Bool,
migrationBackupPath :: Maybe FilePath
migrationBackupPath :: Maybe FilePath,
maintenance :: Bool
}
data CreateBotOpts = CreateBotOpts
@@ -245,6 +245,12 @@ coreChatOptsP appDir defaultDbName = do
<> help "Automatically confirm \"up\" database migrations"
)
migrationBackupPath <- migrationBackupPathP
maintenance <-
switch
( long "maintenance"
<> short 'm'
<> help "Run in maintenance mode (/_start to start chat)"
)
pure
CoreChatOpts
{ dbOptions,
@@ -271,7 +277,8 @@ coreChatOptsP appDir defaultDbName = do
deviceName,
highlyAvailable,
yesToUpMigrations,
migrationBackupPath
migrationBackupPath,
maintenance
}
where
useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 7 (const 15) p
@@ -376,12 +383,6 @@ chatOptsP appDir defaultDbName = do
( long "create-bot-allow-files"
<> help "Flag for created bot to allow files (only allowed together with --create-bot option)"
)
maintenance <-
switch
( long "maintenance"
<> short 'm'
<> help "Run in maintenance mode (/_start to start chat)"
)
pure
ChatOpts
{ coreOptions,
@@ -400,8 +401,7 @@ chatOptsP appDir defaultDbName = do
Just botDisplayName -> Just CreateBotOpts {botDisplayName, allowFiles = createBotAllowFiles}
Nothing
| createBotAllowFiles -> error "--create-bot-allow-files option requires --create-bot-name option"
| otherwise -> Nothing,
maintenance
| otherwise -> Nothing
}
parseProtocolServers :: ProtocolTypeI p => ReadM [ProtoServerWithAuth p]

View File

@@ -1133,7 +1133,7 @@ getGroupInvitation db vr user groupId =
createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> SubscriptionMode -> ExceptT StoreError IO GroupMember
createNewContactMember _ _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ _ _ = throwError $ SEContactNotReady localDisplayName
createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile, activeConn = Just Connection {connChatVersion, peerChatVRange}} memberRole agentConnId connRequest subMode =
createWithRandomId' gVar $ \memId -> runExceptT $ do
createWithRandomId' db gVar $ \memId -> runExceptT $ do
createdAt <- liftIO getCurrentTime
member@GroupMember {groupMemberId} <- createMember_ (MemberId memId) createdAt
void $ liftIO $ createMemberConnection_ db userId groupMemberId agentConnId connChatVersion peerChatVRange Nothing 0 createdAt subMode
@@ -1188,7 +1188,7 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId,
createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionChat -> VersionRangeChat -> SubscriptionMode -> ExceptT StoreError IO ()
createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) chatV peerChatVRange subMode =
createWithRandomId' gVar $ \memId -> runExceptT $ do
createWithRandomId' db gVar $ \memId -> runExceptT $ do
createdAt <- liftIO getCurrentTime
insertMember_ (MemberId memId) createdAt
groupMemberId <- liftIO $ insertedRowId db
@@ -1233,7 +1233,7 @@ createJoiningMember
"INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
(displayName, fullName, shortDescr, image, contactLink, userId, preferences, currentTs, currentTs)
profileId <- liftIO $ insertedRowId db
createWithRandomId' gVar $ \memId -> runExceptT $ do
createWithRandomId' db gVar $ \memId -> runExceptT $ do
insertMember_ ldn profileId (MemberId memId) currentTs
groupMemberId <- liftIO $ insertedRowId db
pure (groupMemberId, MemberId memId)
@@ -1318,7 +1318,7 @@ createBusinessRequestGroup
VersionRange minV maxV = cReqChatVRange
insertClientMember_ currentTs groupId membership =
ExceptT . withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do
createWithRandomId' gVar $ \memId -> runExceptT $ do
createWithRandomId' db gVar $ \memId -> runExceptT $ do
indexInGroup <- getUpdateNextIndexInGroup_ db groupId
liftIO $
DB.execute
@@ -1950,7 +1950,7 @@ getMatchingMemberContacts db vr user@User {userId} GroupMember {memberProfile =
createSentProbe :: DB.Connection -> TVar ChaChaDRG -> UserId -> ContactOrMember -> ExceptT StoreError IO (Probe, Int64)
createSentProbe db gVar userId to =
createWithRandomBytes 32 gVar $ \probe -> do
createWithRandomBytes db 32 gVar $ \probe -> do
currentTs <- getCurrentTime
let (ctId, gmId) = contactOrMemberIds to
DB.execute

View File

@@ -174,6 +174,7 @@ import Simplex.Chat.Types
import Simplex.Chat.Types.Shared
import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, MsgMeta (..), UserId)
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, firstRow', maybeFirstRow)
import Simplex.Messaging.Agent.Store.Common (withSavepoint)
import Simplex.Messaging.Agent.Store.DB (BoolInt (..))
import qualified Simplex.Messaging.Agent.Store.DB as DB
import qualified Simplex.Messaging.Crypto as C
@@ -219,7 +220,7 @@ deleteGroupChatItemsMessages db User {userId} GroupInfo {groupId} = do
createNewSndMessage :: MsgEncodingI e => DB.Connection -> TVar ChaChaDRG -> ConnOrGroupId -> ChatMsgEvent e -> (SharedMsgId -> EncodedChatMessage) -> ExceptT StoreError IO SndMessage
createNewSndMessage db gVar connOrGroupId chatMsgEvent encodeMessage =
createWithRandomId' gVar $ \sharedMsgId ->
createWithRandomId' db gVar $ \sharedMsgId ->
case encodeMessage (SharedMsgId sharedMsgId) of
ECMLarge -> pure $ Left SELargeMsg
ECMEncoded msgBody -> do
@@ -2700,12 +2701,14 @@ updateGroupCIMentions :: DB.Connection -> GroupInfo -> ChatItem 'CTGroup d -> Ma
updateGroupCIMentions db g ci@ChatItem {mentions} mentions'
| mentions' == mentions = pure ci
| otherwise = do
unless (null mentions) $ deleteMentions
unless (null mentions) deleteMentions
if null mentions'
then pure ci
else -- This is a fallback for the error that should not happen in practice.
-- In theory, it may happen in item mentions in database are different from item record.
createMentions `E.catch` \e -> if constraintError e then deleteMentions >> createMentions else E.throwIO e
withSavepoint db "create_mentions" createMentions >>= \case
Right r -> pure r
Left e -> if constraintError e then deleteMentions >> createMentions else E.throwIO e
where
deleteMentions = DB.execute db "DELETE FROM chat_item_mentions WHERE chat_item_id = ?" (Only $ chatItemId' ci)
createMentions = createGroupCIMentions db g ci mentions'

View File

@@ -40,6 +40,7 @@ import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (AConnShortLink (..), AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionRequestUri, CreatedConnLink (..), UserId, connMode)
import Simplex.Messaging.Agent.Store (AnyStoreError (..))
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow)
import Simplex.Messaging.Agent.Store.Common (withSavepoint)
import Simplex.Messaging.Agent.Store.DB (BoolInt (..))
import qualified Simplex.Messaging.Agent.Store.DB as DB
import qualified Simplex.Messaging.Crypto as C
@@ -597,42 +598,23 @@ withLocalDisplayName db userId displayName action = getLdnSuffix >>= (`tryCreate
|]
(ldn, displayName, ldnSuffix, userId, ts, ts)
-- Execute an action within a savepoint (PostgreSQL only).
-- On success, releases the savepoint. On error, rolls back to the savepoint
-- to restore the transaction to a usable state before returning the error.
withSavepoint :: DB.Connection -> Query -> IO a -> IO (Either SQLError a)
withSavepoint db name action =
#if defined(dbPostgres)
do
DB.execute_ db $ "SAVEPOINT " <> name
E.try action >>= \case
Right r -> do
DB.execute_ db $ "RELEASE SAVEPOINT " <> name
pure $ Right r
Left e -> do
DB.execute_ db $ "ROLLBACK TO SAVEPOINT " <> name
pure $ Left e
#else
E.try action
#endif
createWithRandomId :: forall a. DB.Connection -> TVar ChaChaDRG -> (ByteString -> IO a) -> ExceptT StoreError IO a
createWithRandomId db = createWithRandomBytes db 12
createWithRandomId :: forall a. TVar ChaChaDRG -> (ByteString -> IO a) -> ExceptT StoreError IO a
createWithRandomId = createWithRandomBytes 12
createWithRandomId' :: forall a. DB.Connection -> TVar ChaChaDRG -> (ByteString -> IO (Either StoreError a)) -> ExceptT StoreError IO a
createWithRandomId' db = createWithRandomBytes' db 12
createWithRandomId' :: forall a. TVar ChaChaDRG -> (ByteString -> IO (Either StoreError a)) -> ExceptT StoreError IO a
createWithRandomId' = createWithRandomBytes' 12
createWithRandomBytes :: forall a. DB.Connection -> Int -> TVar ChaChaDRG -> (ByteString -> IO a) -> ExceptT StoreError IO a
createWithRandomBytes db size gVar create = createWithRandomBytes' db size gVar (fmap Right . create)
createWithRandomBytes :: forall a. Int -> TVar ChaChaDRG -> (ByteString -> IO a) -> ExceptT StoreError IO a
createWithRandomBytes size gVar create = createWithRandomBytes' size gVar (fmap Right . create)
createWithRandomBytes' :: forall a. Int -> TVar ChaChaDRG -> (ByteString -> IO (Either StoreError a)) -> ExceptT StoreError IO a
createWithRandomBytes' size gVar create = tryCreate 3
createWithRandomBytes' :: forall a. DB.Connection -> Int -> TVar ChaChaDRG -> (ByteString -> IO (Either StoreError a)) -> ExceptT StoreError IO a
createWithRandomBytes' db size gVar create = tryCreate 3
where
tryCreate :: Int -> ExceptT StoreError IO a
tryCreate 0 = throwError SEUniqueID
tryCreate n = do
id' <- liftIO $ encodedRandomBytes gVar size
liftIO (E.try $ create id') >>= \case
liftIO (withSavepoint db "create_random_id" (create id')) >>= \case
Right x -> liftEither x
Left e
| constraintError e -> tryCreate (n - 1)

View File

@@ -117,8 +117,7 @@ testOpts =
autoAcceptFileSize = 0,
muteNotifications = True,
markRead = True,
createBot = Nothing,
maintenance = False
createBot = Nothing
}
testCoreOpts :: CoreChatOpts
@@ -152,7 +151,8 @@ testCoreOpts =
deviceName = Nothing,
highlyAvailable = False,
yesToUpMigrations = False,
migrationBackupPath = Nothing
migrationBackupPath = Nothing,
maintenance = False
}
#if !defined(dbPostgres)
@@ -303,7 +303,7 @@ insertUser st = withTransaction st (`DB.execute_` "INSERT INTO users (user_id) V
#endif
startTestChat_ :: TestParams -> ChatDatabase -> ChatConfig -> ChatOpts -> User -> IO TestCC
startTestChat_ TestParams {printOutput} db cfg opts@ChatOpts {maintenance} user = do
startTestChat_ TestParams {printOutput} db cfg opts@ChatOpts {coreOptions = CoreChatOpts {maintenance}} user = do
t <- withVirtualTerminal termSettings pure
ct <- newChatTerminal t opts
cc <- newChatController db (Just user) cfg opts False

View File

@@ -1352,7 +1352,7 @@ testNegotiateCall =
testMaintenanceMode :: HasCallStack => TestParams -> IO ()
testMaintenanceMode ps = do
withNewTestChat ps "bob" bobProfile $ \bob -> do
withNewTestChatOpts ps testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do
withNewTestChatOpts ps testOpts {coreOptions = testCoreOpts {maintenance = True}} "alice" aliceProfile $ \alice -> do
alice ##> "/c"
alice <## "error: chat not started"
alice ##> "/_start"
@@ -1397,7 +1397,7 @@ testChatWorking alice bob = do
testMaintenanceModeWithFiles :: HasCallStack => TestParams -> IO ()
testMaintenanceModeWithFiles ps = withXFTPServer $ do
withNewTestChat ps "bob" bobProfile $ \bob -> do
withNewTestChatOpts ps testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do
withNewTestChatOpts ps testOpts {coreOptions = testCoreOpts {maintenance = True}} "alice" aliceProfile $ \alice -> do
alice ##> "/_start"
alice <## "chat started"
alice ##> "/_files_folder ./tests/tmp/alice_files"
@@ -1655,7 +1655,7 @@ testSubscribeAppNSE :: HasCallStack => TestParams -> IO ()
testSubscribeAppNSE ps =
withNewTestChat ps "bob" bobProfile $ \bob -> do
withNewTestChat ps "alice" aliceProfile $ \alice -> do
withTestChatOpts ps testOpts {maintenance = True} "alice" $ \nseAlice -> do
withTestChatOpts ps testOpts {coreOptions = testCoreOpts {maintenance = True}} "alice" $ \nseAlice -> do
alice ##> "/_app suspend 1"
alice <## "ok"
alice <## "chat suspended"

View File

@@ -14,6 +14,7 @@
"textColor": "white",
"iconBg": "green",
"enabled": true,
"home": true,
"rtl": true
},
{

View File

@@ -7,8 +7,8 @@
{% endfor %}
<header id="navbar">
<a href="/{{ '' if lang == 'en' else lang }}" class="logo flex dark:hidden ltr:mr-auto rtl:ml-auto"><img src="/img/new/logo-light.png" alt="logo"></a>
<a href="/{{ '' if lang == 'en' else lang }}" class="logo hidden dark:flex ltr:mr-auto rtl:ml-auto"><img src="/img/new/logo-dark.png" alt="logo"></a>
<a href="/{{ '' if lang == 'en' else lang }}" class="logo logo-light ltr:mr-auto rtl:ml-auto"><img src="/img/new/logo-light.png" alt="logo"></a>
<a href="/{{ '' if lang == 'en' else lang }}" class="logo logo-dark ltr:mr-auto rtl:ml-auto"><img src="/img/new/logo-dark.png" alt="logo"></a>
<nav id="menu">
<ul>

View File

@@ -15,9 +15,17 @@ body.change-nav-color {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
[dir="ltr"] .logo {
border-bottom-right-radius: 12px;
padding-right: 24px;
transition: all 0.3s ease;
}
[dir="rtl"] .logo {
border-bottom-left-radius: 12px;
padding-left: 24px;
}
.dark .logo {
@@ -42,9 +50,17 @@ body.change-nav-color {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
[dir="ltr"] .right-links {
border-bottom-left-radius: 12px;
padding-left: 24px;
transition: all 0.3s ease;
}
[dir="rtl"] .right-links {
border-bottom-right-radius: 12px;
padding-right: 24px;
}
.dark .right-links {
@@ -69,14 +85,46 @@ header#navbar {
header#navbar>a.logo {
position: absolute;
left: 0px;
padding-left: 30px;
padding-top: 2px;
height: 100%;
/* display: flex; */
align-items: center;
}
[dir="ltr"] header#navbar>a.logo {
left: 0px;
padding-left: 30px;
}
[dir="rtl"] header#navbar>a.logo {
right: 0px;
padding-right: 30px;
}
header#navbar>a.logo.logo-light {
display: flex;
}
header#navbar>a.logo.logo-dark {
display: none;
}
.dark header#navbar>a.logo.logo-light {
display: none;
}
.dark header#navbar>a.logo.logo-dark {
display: flex;
}
[dir="rtl"].dark .change-nav-color header#navbar>a.logo.logo-light {
display: flex;
}
[dir="rtl"].dark .change-nav-color header#navbar>a.logo.logo-dark {
display: none;
}
header#navbar>a.logo img {
height: 40px;
width: auto;
@@ -183,7 +231,7 @@ header#navbar ul a {
color: black;
}
.change-nav-color .box-btn.token {
.dark .change-nav-color .box-btn.token {
background: linear-gradient(90deg, #019bfe 0%, #2e3fa0 100%);
color: white;
}
@@ -219,9 +267,13 @@ header#navbar ul a span svg {
fill: var(--nav-color);
}
header#navbar nav#menu li.nav-link:hover span svg {
[dir="ltr"] header#navbar nav#menu li.nav-link:hover span svg {
transform: rotate(180deg);
}
[dir="rtl"] header#navbar nav#menu li.nav-link:hover span svg {
transform: rotate(-180deg);
}
}
header#navbar ul.sub-menu {
@@ -348,6 +400,11 @@ header#navbar .nav-link:focus-within .sub-menu {
right: 0;
}
[dir="rtl"] .flag-container .sub-menu {
left: 0;
right: auto;
}
header#navbar button.theme-switch-btn {
display: flex;
align-items: center;
@@ -380,14 +437,22 @@ header#navbar button.theme-switch-btn svg path {
.right-links {
position: absolute;
top: 0;
right: 0px;
padding-right: 20px;
height: 54px;
display: flex;
align-items: center;
gap: 1.5rem;
}
[dir="ltr"] .right-links {
right: 0px;
padding-right: 20px;
}
[dir="rtl"] .right-links {
left: 0px;
padding-left: 20px;
}
button#cross-btn {
background-color: transparent;
border: none;
@@ -454,9 +519,16 @@ button#cross-btn {
visibility: hidden;
opacity: 0;
transition: all .3s ease;
}
[dir="ltr"] header#navbar {
transform: translateX(-100%);
}
[dir="rtl"] header#navbar {
transform: translateX(100%);
}
.dark header#navbar {
background: #0a0f2b;
}
@@ -494,13 +566,20 @@ button#cross-btn {
header#navbar nav#menu {
position: fixed !important;
top: 54px;
left: 0;
width: 100%;
height: calc(100% - 54px);
overflow: auto;
padding: 0.5rem 0;
}
[dir="ltr"] header#navbar nav#menu {
left: 0;
}
[dir="rtl"] header#navbar nav#menu {
right: 0;
}
header#navbar nav#menu .nav-link {
width: 100%;
}

View File

@@ -433,7 +433,6 @@ section.cover div.content p {
.publications-btns {
position: absolute;
bottom: 24px;
left: 30px;
display: flex;
flex-wrap: wrap;
align-items: center;
@@ -441,6 +440,14 @@ section.cover div.content p {
gap: 12px;
}
[dir="ltr"] .publications-btns {
left: 30px;
}
[dir="rtl"] .publications-btns {
right: 30px;
}
.publications-btns img {
width: 32px;
}
@@ -456,8 +463,6 @@ section.cover div.content p {
.security-btns {
position: absolute;
bottom: 24px;
left: 0;
transform: translateX(calc((100vw / 2) - 50%));
display: flex;
flex-wrap: wrap;
align-items: center;
@@ -465,6 +470,16 @@ section.cover div.content p {
gap: 12px;
}
[dir="ltr"] .security-btns {
left: 0;
transform: translateX(calc((100vw / 2) - 50%));
}
[dir="rtl"] .security-btns {
right: 0;
transform: translateX(calc(-1 * ((100vw / 2) - 50%)));
}
.security-btns img {
height: 36px;
}
@@ -473,27 +488,38 @@ section.cover div.content p {
font-size: 14px !important;
font-family: 'Manrope', 'GT-Walsheim', sans-serif !important;
font-weight: 300 !important;
text-align: left;
color: white;
line-height: 1.2;
}
[dir="ltr"] .security-audits {
text-align: left;
}
[dir="rtl"] .security-audits {
text-align: right;
}
@media screen and (max-width: 1279px) {
.publications-btns {
display: none;
visibility: hidden;
}
.security-btns {
[dir="ltr"] .security-btns {
transform: translateX(0);
left: 16px;
}
[dir="rtl"] .security-btns {
transform: translateX(0);
right: 16px;
}
}
.socials {
position: absolute;
bottom: 22px;
right: 30px;
display: flex;
align-items: center;
justify-content: center;
@@ -501,6 +527,14 @@ section.cover div.content p {
flex-wrap: wrap;
}
[dir="ltr"] .socials {
right: 30px;
}
[dir="rtl"] .socials {
left: 30px;
}
.socials a img {
width: auto;
height: 40px;
@@ -783,6 +817,12 @@ main .section-bg {
margin-left: auto;
}
[dir="rtl"] .page-2 .text-container,
[dir="rtl"] .page-5 .text-container,
[dir="rtl"] .page-6 .text-container {
margin-right: auto;
}
.page-4 .text-container h2 {
max-width: calc(var(--sec-vwu)*26) !important;
}
@@ -929,6 +969,10 @@ main .section-bg {
float: left;
}
[dir="rtl"] .roadmap>p:first-child {
float: right;
}
.roadmap p.title span {
display: inline;
}

View File

@@ -166,17 +166,17 @@ active_home: true
<h2>{{ "index-roadmap-h2" | i18n({}, lang) }}</h2>
<div class="roadmap">
<p>{{ "index-roadmap-2025" | i18n({}, lang) }} </p>
<p class="title"><span>: </span>{{ "index-roadmap-2025-title" | i18n({}, lang) }}</p>
<p class="title"><span class="ltr:order-first rtl:order-last">: </span>{{ "index-roadmap-2025-title" | i18n({}, lang) }}</p>
<p>{{ "index-roadmap-2025-desc" | i18n({}, lang) }}</p>
</div>
<div class="roadmap">
<p>{{ "index-roadmap-2026" | i18n({}, lang) }} </p>
<p class="title"><span>: </span>{{ "index-roadmap-2026-title" | i18n({}, lang) }}</p>
<p class="title"><span class="ltr:order-first rtl:order-last">: </span>{{ "index-roadmap-2026-title" | i18n({}, lang) }}</p>
<p>{{ "index-roadmap-2026-desc" | i18n({}, lang) }}</p>
</div>
<div class="roadmap">
<p>{{ "index-roadmap-2027" | i18n({}, lang) }} </p>
<p class="title"><span>: </span>{{ "index-roadmap-2027-title" | i18n({}, lang) }}</p>
<p class="title"><span class="ltr:order-first rtl:order-last">: </span>{{ "index-roadmap-2027-title" | i18n({}, lang) }}</p>
<p>{{ "index-roadmap-2027-desc" | i18n({}, lang) }}</p>
</div>
</div>
@@ -235,19 +235,19 @@ active_home: true
(function navColorOnScroll(){
const header = document.querySelector('header#navbar');
const cover = document.querySelector('.page-1.cover');
if (!header || !cover || !('IntersectionObserver' in window)) {
const footer = document.querySelector('.footer.page');
if (!header || !cover || !footer || !('IntersectionObserver' in window)) {
window.addEventListener('load', () => header && document.body.classList.toggle('change-nav-color', window.scrollY > 10));
window.addEventListener('scroll', () => header && document.body.classList.toggle('change-nav-color', window.scrollY > 10));
return;
}
const io = new IntersectionObserver((entries) => {
for (const e of entries) {
// if cover is mostly visible, keep white nav; else switch to #001796
const onCover = e.isIntersecting && e.intersectionRatio >= 0.02;
document.body.classList.toggle('change-nav-color', !onCover);
}
// if cover or footer is mostly visible, keep white nav; else switch to #001796
const onObserved = entries.some((e) => e.isIntersecting && e.intersectionRatio >= 0.02);
document.body.classList.toggle('change-nav-color', !onObserved);
}, { threshold: [0, 0.02, 1] });
io.observe(cover);
io.observe(footer);
})();
</script>
<script>