Merge branch 'master' into master-ios

This commit is contained in:
spaced4ndy
2023-06-12 13:47:27 +04:00
31 changed files with 783 additions and 97 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
node-version: [12.x]
node-version: [16.x]
steps:
- uses: actions/checkout@v2
+2 -2
View File
@@ -30,7 +30,7 @@ In a more narrow sense, particularly in media, blockchain is used to refer speci
Centralized networks are provided or controlled by a single entity. The examples are Threema, Signal, WhatsApp and Telegram. The advantage of that design is that the provider can innovate faster, and has a centralized approach to security. But the disadvantage is that the provider can change or discontinue the service, and leak, sell or disclose in some other way all users' data, including who they are connected with.
## Content padding
## Content padding
[Message padding](#message-padding).
@@ -149,7 +149,7 @@ Generalizing [the definition](https://csrc.nist.gov/glossary/term/pairwise_pseud
In the context of SimpleX network, these are the identifiers generated by SMP relays to access anonymous messaging queues, with a separate identifier (and access credential) for each accessing party: recipient, sender and and optional notifications subscriber. The same approach is used by XFTP relays to access file chunks, with separate identifiers (and access credentials) for sender and each recipient.
## Peer-to-peer
## Peer-to-peer
Peer-to-peer (P2P) is the network architecture when participants have equal rights and communicate directly via a general purpose transport or overlay network. Unlike client-server architecture, all peers in a P2P network both provide and consume the resources. In the context of messaging, P2P architecture usually means that the messages are sent between peers, without user accounts or messages being stored on any servers. Examples are Tox, Briar, Cwtch and many others.
+3 -3
View File
@@ -1,15 +1,15 @@
---
title: Hosting your own SMP Server
revision: 31.01.2023
revision: 05.06.2023
---
| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) |
| Updated 05.06.2023 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) |
# Hosting your own SMP Server
## Overview
SMP server is the relay server used to pass messages in SimpleX network. SimpleX Chat apps have preset servers (for mobile apps these are smp8, smp9 and smp10.simplex.im), but you can easily change app configuration to use other servers.
SMP server is the relay server used to pass messages in SimpleX network. SimpleX Chat apps have preset servers (for mobile apps these are smp11, smp12 and smp14.simplex.im), but you can easily change app configuration to use other servers.
SimpleX clients only determine which server is used to receive the messages, separately for each contact (or group connection with a group member), and these servers are only temporary, as the delivery address can change.
+3 -3
View File
@@ -1,14 +1,14 @@
---
title: Hostování vlastního serveru SMP
revision: 31.01.2023
revision: 05.06.2023
---
| Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/SERVER.md), [FR](/docs/lang/fr/SERVER.md) |
| Aktualizováno 05.06.2023 | Jazyky: CZ, [EN](/docs/SERVER.md), [FR](/docs/lang/fr/SERVER.md) |
# Hostování vlastního serveru SMP
## Přehled
SMP server je relay server používaný k předávání zpráv v síti SimpleX. Aplikace SimpleX Chat mají přednastavené servery (pro mobilní aplikace jsou to smp8, smp9 a smp10.simplex.im), ale konfiguraci aplikace můžete snadno změnit a používat jiné servery.
SMP server je relay server používaný k předávání zpráv v síti SimpleX. Aplikace SimpleX Chat mají přednastavené servery (pro mobilní aplikace jsou to smp11, smp12 a smp14.simplex.im), ale konfiguraci aplikace můžete snadno změnit a používat jiné servery.
Klienti SimpleX pouze určují, který server bude použit pro příjem zpráv, a to pro každý kontakt (nebo spojení skupiny s členem skupiny) zvlášť, přičemž tyto servery jsou pouze dočasné, protože adresa pro doručování se může změnit.
+3 -3
View File
@@ -1,14 +1,14 @@
---
title: Héberger votre propre serveur SMP
revision: 31.01.2023
revision: 05.06.2023
---
| 31.01.2023 | FR, [EN](/docs/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) |
| 05.06.2023 | FR, [EN](/docs/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) |
# Héberger votre propre serveur SMP
## Présentation générale
Un serveur SMP est un serveur relais utilisé pour transmettre les messages sur le réseau SimpleX. Les apps SimpleX Chat ont des serveurs prédéfinis (pour les apps mobiles, smp8, smp9 et smp10.simplex.im), mais vous pouvez facilement modifier la configuration de l'app pour utiliser d'autres serveurs.
Un serveur SMP est un serveur relais utilisé pour transmettre les messages sur le réseau SimpleX. Les apps SimpleX Chat ont des serveurs prédéfinis (pour les apps mobiles, smp11, smp12 et smp14.simplex.im), mais vous pouvez facilement modifier la configuration de l'app pour utiliser d'autres serveurs.
Seuls les utilisateurs de SimpleX déterminent quel serveur est utilisé pour recevoir les messages, séparément pour chaque contact (ou pour chaque connexion à un membre d'un groupe), et ces serveurs ne sont que temporaires, car l'adresse de réception peut changer.
+1 -1
View File
@@ -1,5 +1,5 @@
name: simplex-chat
version: 5.1.2.0
version: 5.1.3.0
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
+2 -1
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
version: 5.1.2.0
version: 5.1.3.0
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
@@ -100,6 +100,7 @@ library
Simplex.Chat.Migrations.M20230519_item_deleted_ts
Simplex.Chat.Migrations.M20230526_indexes
Simplex.Chat.Migrations.M20230529_indexes
Simplex.Chat.Migrations.M20230608_deleted_contacts
Simplex.Chat.Mobile
Simplex.Chat.Mobile.WebRTC
Simplex.Chat.Options
+23 -11
View File
@@ -121,15 +121,16 @@ defaultChatConfig =
testView = False,
initialCleanupManagerDelay = 30 * 1000000, -- 30 seconds
cleanupManagerInterval = 30 * 60, -- 30 minutes
cleanupManagerStepDelay = 3 * 1000000, -- 3 seconds
ciExpirationInterval = 30 * 60 * 1000000 -- 30 minutes
}
_defaultSMPServers :: NonEmpty SMPServerWithAuth
_defaultSMPServers =
L.fromList
[ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion",
"smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion",
"smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion"
[ "smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion",
"smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion",
"smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion"
]
_defaultNtfServers :: [NtfServer]
@@ -867,7 +868,7 @@ processChatCommand = \case
Just _ -> pure []
Nothing -> do
conns <- withStore $ \db -> getContactConnections db userId ct
withStore' (\db -> deleteContactWithoutGroups db user ct)
withStore' (\db -> setContactDeleted db user ct)
`catchError` (toView . CRChatError (Just user))
pure $ map aConnId conns
CTContactRequest -> pure $ chatCmdError (Just user) "not supported"
@@ -2358,15 +2359,16 @@ cleanupManager :: forall m. ChatMonad m => m ()
cleanupManager = do
interval <- asks (cleanupManagerInterval . config)
runWithoutInitialDelay interval
delay <- asks (initialCleanupManagerDelay . config)
liftIO $ threadDelay' delay
initialDelay <- asks (initialCleanupManagerDelay . config)
liftIO $ threadDelay' initialDelay
stepDelay <- asks (cleanupManagerStepDelay . config)
forever $ do
flip catchError (toView . CRChatError Nothing) $ do
waitChatStarted
users <- withStoreCtx' (Just "cleanupManager, getUsers 1") getUsers
let (us, us') = partition activeUser users
forM_ us $ cleanupUser interval
forM_ us' $ cleanupUser interval
forM_ us $ cleanupUser interval stepDelay
forM_ us' $ cleanupUser interval stepDelay
cleanupMessages `catchError` (toView . CRChatError Nothing)
liftIO $ threadDelay' $ diffToMicroseconds interval
where
@@ -2376,13 +2378,21 @@ cleanupManager = do
let (us, us') = partition activeUser users
forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchError` (toView . CRChatError (Just u))
forM_ us' $ \u -> cleanupTimedItems cleanupInterval u `catchError` (toView . CRChatError (Just u))
cleanupUser cleanupInterval user =
cleanupUser cleanupInterval stepDelay user = do
cleanupTimedItems cleanupInterval user `catchError` (toView . CRChatError (Just user))
liftIO $ threadDelay' stepDelay
cleanupDeletedContacts user `catchError` (toView . CRChatError (Just user))
liftIO $ threadDelay' stepDelay
cleanupTimedItems cleanupInterval user = do
ts <- liftIO getCurrentTime
let startTimedThreadCutoff = addUTCTime cleanupInterval ts
timedItems <- withStoreCtx' (Just "cleanupManager, getTimedItems") $ \db -> getTimedItems db user startTimedThreadCutoff
forM_ timedItems $ \(itemRef, deleteAt) -> startTimedItemThread user itemRef deleteAt `catchError` const (pure ())
cleanupDeletedContacts user = do
contacts <- withStore' (`getDeletedContacts` user)
forM_ contacts $ \ct ->
withStore' (\db -> deleteContactWithoutGroups db user ct)
`catchError` (toView . CRChatError (Just user))
cleanupMessages = do
ts <- liftIO getCurrentTime
let cutoffTs = addUTCTime (- (30 * nominalDay)) ts
@@ -4295,7 +4305,9 @@ throwChatError = throwError . ChatError
deleteMembersConnections :: ChatMonad m => User -> [GroupMember] -> m ()
deleteMembersConnections user members = do
let memberConns = mapMaybe (\GroupMember {activeConn} -> activeConn) members
let memberConns =
filter (\Connection {connStatus} -> connStatus /= ConnDeleted) $
mapMaybe (\GroupMember {activeConn} -> activeConn) members
deleteAgentConnectionsAsync user $ map aConnId memberConns
forM_ memberConns $ \conn -> withStore' $ \db -> updateConnectionStatus db conn ConnDeleted
@@ -4719,7 +4731,6 @@ withStoreCtx ctx_ action = do
ChatController {chatStore} <- ask
liftEitherError ChatErrorStore $ case ctx_ of
Nothing -> withTransaction chatStore (runExceptT . action) `E.catch` handleInternal ""
Just _ -> withTransaction chatStore (runExceptT . action) `E.catch` handleInternal ""
-- uncomment to debug store performance
-- Just ctx -> do
-- t1 <- liftIO getCurrentTime
@@ -4728,6 +4739,7 @@ withStoreCtx ctx_ action = do
-- t2 <- liftIO getCurrentTime
-- putStrLn $ "withStoreCtx end :: " <> show t2 <> " :: " <> ctx <> " :: duration=" <> show (diffToMilliseconds $ diffUTCTime t2 t1)
-- pure r
Just _ -> withTransaction chatStore (runExceptT . action) `E.catch` handleInternal ""
where
handleInternal :: String -> E.SomeException -> IO (Either StoreError a)
handleInternal ctxStr e = pure . Left . SEInternalError $ show e <> ctxStr
+1
View File
@@ -112,6 +112,7 @@ data ChatConfig = ChatConfig
testView :: Bool,
initialCleanupManagerDelay :: Int64,
cleanupManagerInterval :: NominalDiffTime,
cleanupManagerStepDelay :: Int64,
ciExpirationInterval :: Int64 -- microseconds
}
@@ -0,0 +1,22 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20230608_deleted_contacts where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20230608_deleted_contacts :: Query
m20230608_deleted_contacts =
[sql|
ALTER TABLE contacts ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0;
CREATE INDEX msg_delivery_events_msg_delivery_id ON msg_delivery_events(msg_delivery_id);
|]
down_m20230608_deleted_contacts :: Query
down_m20230608_deleted_contacts =
[sql|
DROP INDEX msg_delivery_events_msg_delivery_id;
ALTER TABLE contacts DROP COLUMN deleted;
|]
@@ -63,6 +63,7 @@ CREATE TABLE contacts(
contact_used INTEGER DEFAULT 0 CHECK(contact_used NOT NULL),
user_preferences TEXT DEFAULT '{}' CHECK(user_preferences NOT NULL),
chat_ts TEXT,
deleted INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
@@ -652,3 +653,6 @@ CREATE INDEX idx_msg_deliveries_agent_ack_cmd_id ON msg_deliveries(
connection_id,
agent_ack_cmd_id
);
CREATE INDEX msg_delivery_events_msg_delivery_id ON msg_delivery_events(
msg_delivery_id
);
+42 -19
View File
@@ -52,6 +52,8 @@ module Simplex.Chat.Store
deleteContactConnectionsAndFiles,
deleteContact,
deleteContactWithoutGroups,
setContactDeleted,
getDeletedContacts,
getContactByName,
getContact,
getContactIdByName,
@@ -395,6 +397,7 @@ import Simplex.Chat.Migrations.M20230511_reactions
import Simplex.Chat.Migrations.M20230519_item_deleted_ts
import Simplex.Chat.Migrations.M20230526_indexes
import Simplex.Chat.Migrations.M20230529_indexes
import Simplex.Chat.Migrations.M20230608_deleted_contacts
import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Chat.Util (week)
@@ -476,7 +479,8 @@ schemaMigrations =
("20230511_reactions", m20230511_reactions, Just down_m20230511_reactions),
("20230519_item_deleted_ts", m20230519_item_deleted_ts, Just down_m20230519_item_deleted_ts),
("20230526_indexes", m20230526_indexes, Just down_m20230526_indexes),
("20230529_indexes", m20230529_indexes, Just down_m20230529_indexes)
("20230529_indexes", m20230529_indexes, Just down_m20230529_indexes),
("20230608_deleted_contacts", m20230608_deleted_contacts, Just down_m20230608_deleted_contacts)
]
-- | The list of migrations in ascending order by date
@@ -547,7 +551,7 @@ getUsersInfo db = getUsers db >>= mapM getUserInfo
SELECT COUNT(1)
FROM chat_items i
JOIN contacts ct USING (contact_id)
WHERE i.user_id = ? AND i.item_status = ? AND (ct.enable_ntfs = 1 OR ct.enable_ntfs IS NULL)
WHERE i.user_id = ? AND i.item_status = ? AND (ct.enable_ntfs = 1 OR ct.enable_ntfs IS NULL) AND ct.deleted = 0
|]
(userId, CISRcvNew)
gCount <-
@@ -622,7 +626,7 @@ getUserByARcvFileId db aRcvFileId =
getUserByContactId :: DB.Connection -> ContactId -> ExceptT StoreError IO User
getUserByContactId db contactId =
ExceptT . firstRow toUser (SEUserNotFoundByContactId contactId) $
DB.query db (userQuery <> " JOIN contacts ct ON ct.user_id = u.user_id WHERE ct.contact_id = ?") (Only contactId)
DB.query db (userQuery <> " JOIN contacts ct ON ct.user_id = u.user_id WHERE ct.contact_id = ? AND ct.deleted = 0") (Only contactId)
getUserByGroupId :: DB.Connection -> GroupId -> ExceptT StoreError IO User
getUserByGroupId db groupId =
@@ -711,7 +715,7 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ? AND c.via_contact_uri_hash = ?
WHERE ct.user_id = ? AND c.via_contact_uri_hash = ? AND ct.deleted = 0
ORDER BY c.connection_id DESC
LIMIT 1
|]
@@ -757,7 +761,6 @@ getProfileById db userId profileId =
[sql|
SELECT cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, cp.preferences -- , ct.user_preferences
FROM contact_profiles cp
-- JOIN contacts ct ON cp.contact_profile_id = ct.contact_profile_id
WHERE cp.user_id = ? AND cp.contact_profile_id = ?
|]
(userId, profileId)
@@ -849,6 +852,19 @@ deleteContactWithoutGroups db user@User {userId} Contact {contactId, localDispla
DB.execute db "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?" (userId, contactId)
forM_ customUserProfileId $ \profileId -> deleteUnusedIncognitoProfileById_ db user profileId
setContactDeleted :: DB.Connection -> User -> Contact -> IO ()
setContactDeleted db User {userId} Contact {contactId} = do
currentTs <- getCurrentTime
DB.execute db "UPDATE contacts SET deleted = 1, updated_at = ? WHERE user_id = ? AND contact_id = ?" (currentTs, userId, contactId)
getDeletedContacts :: DB.Connection -> User -> IO [Contact]
getDeletedContacts db user@User {userId} = do
contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 1" (Only userId)
rights <$> mapM (runExceptT . getDeletedContact db user) contactIds
getDeletedContact :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO Contact
getDeletedContact db user contactId = getContact_ db user contactId True
deleteUnusedIncognitoProfileById_ :: DB.Connection -> User -> ProfileId -> IO ()
deleteUnusedIncognitoProfileById_ db User {userId} profile_id =
DB.executeNamed
@@ -1061,7 +1077,7 @@ getContactByName db user localDisplayName = do
getUserContacts :: DB.Connection -> User -> IO [Contact]
getUserContacts db user@User {userId} = do
contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ?" (Only userId)
contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0" (Only userId)
rights <$> mapM (runExceptT . getContact db user) contactIds
-- only used in tests
@@ -1365,7 +1381,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ? AND ct.xcontact_id = ?
WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0
ORDER BY c.connection_id DESC
LIMIT 1
|]
@@ -1615,6 +1631,7 @@ getMatchingContacts db user@User {userId} Contact {contactId, profile = LocalPro
FROM contacts ct
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
WHERE ct.user_id = ? AND ct.contact_id != ?
AND ct.deleted = 0
AND p.display_name = ? AND p.full_name = ?
AND ((p.image IS NULL AND ? IS NULL) OR p.image = ?)
|]
@@ -1657,7 +1674,7 @@ matchReceivedProbe db user@User {userId} _from@Contact {contactId} (Probe probe)
SELECT c.contact_id
FROM contacts c
JOIN received_probes r ON r.contact_id = c.contact_id
WHERE c.user_id = ? AND r.probe_hash = ? AND r.probe IS NULL
WHERE c.user_id = ? AND c.deleted = 0 AND r.probe_hash = ? AND r.probe IS NULL
|]
(userId, probeHash)
currentTs <- getCurrentTime
@@ -1678,7 +1695,7 @@ matchReceivedProbeHash db user@User {userId} _from@Contact {contactId} (ProbeHas
SELECT c.contact_id, r.probe
FROM contacts c
JOIN received_probes r ON r.contact_id = c.contact_id
WHERE c.user_id = ? AND r.probe_hash = ? AND r.probe IS NOT NULL
WHERE c.user_id = ? AND c.deleted = 0 AND r.probe_hash = ? AND r.probe IS NOT NULL
|]
(userId, probeHash)
currentTs <- getCurrentTime
@@ -1703,7 +1720,7 @@ matchSentProbe db user@User {userId} _from@Contact {contactId} (Probe probe) = d
FROM contacts c
JOIN sent_probes s ON s.contact_id = c.contact_id
JOIN sent_probe_hashes h ON h.sent_probe_id = s.sent_probe_id
WHERE c.user_id = ? AND s.probe = ? AND h.contact_id = ?
WHERE c.user_id = ? AND c.deleted = 0 AND s.probe = ? AND h.contact_id = ?
|]
(userId, probe, contactId)
case contactIds of
@@ -1808,7 +1825,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts
FROM contacts c
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
WHERE c.user_id = ? AND c.contact_id = ?
WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0
|]
(userId, contactId)
toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, Maybe Bool) :. (Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime)] -> Either StoreError Contact
@@ -1913,6 +1930,7 @@ getConnectionsContacts db agentConnIds = do
JOIN connections c ON c.contact_id = ct.contact_id
WHERE c.agent_conn_id IN (SELECT conn_id FROM temp.conn_ids)
AND c.conn_type = ?
AND ct.deleted = 0
|]
(Only ConnContact)
DB.execute_ db "DROP TABLE temp.conn_ids"
@@ -2388,7 +2406,7 @@ getContactViaMember db user@User {userId} GroupMember {groupMemberId} =
where cc.contact_id = ct.contact_id
)
JOIN group_members m ON m.contact_id = ct.contact_id
WHERE ct.user_id = ? AND m.group_member_id = ?
WHERE ct.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0
|]
(userId, groupMemberId)
@@ -2697,7 +2715,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} =
FROM connections cc
where cc.group_member_id = m.group_member_id
)
WHERE ct.user_id = ? AND ct.contact_id = ? AND mu.contact_id = ?
WHERE ct.user_id = ? AND ct.contact_id = ? AND mu.contact_id = ? AND ct.deleted = 0
|]
(userId, contactId, userContactId)
where
@@ -2727,7 +2745,7 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} =
)
JOIN groups g ON g.group_id = ct.via_group
JOIN group_members m ON m.group_id = g.group_id AND m.contact_id = ct.contact_id
WHERE ct.user_id = ? AND m.group_member_id = ?
WHERE ct.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0
|]
(userId, groupMemberId)
where
@@ -3826,6 +3844,7 @@ getDirectChatPreviews_ db user@User {userId} = do
LEFT JOIN chat_items ri ON ri.user_id = i.user_id AND ri.contact_id = i.contact_id AND ri.shared_msg_id = i.quoted_shared_msg_id
WHERE ct.user_id = ?
AND ((c.conn_level = 0 AND c.via_group_link = 0) OR ct.contact_used = 1)
AND ct.deleted = 0
AND c.connection_id = (
SELECT cc_connection_id FROM (
SELECT
@@ -4107,10 +4126,13 @@ getDirectChatBefore_ db User {userId} ct@Contact {contactId} beforeChatItemId co
getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64
getContactIdByName db User {userId} cName =
ExceptT . firstRow fromOnly (SEContactNotFoundByName cName) $
DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND local_display_name = ?" (userId, cName)
DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND local_display_name = ? AND deleted = 0" (userId, cName)
getContact :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO Contact
getContact db user@User {userId} contactId =
getContact db user contactId = getContact_ db user contactId False
getContact_ :: DB.Connection -> User -> Int64 -> Bool -> ExceptT StoreError IO Contact
getContact_ db user@User {userId} contactId deleted =
ExceptT . fmap join . firstRow (toContactOrError user) (SEContactNotFound contactId) $
DB.query
db
@@ -4126,6 +4148,7 @@ getContact db user@User {userId} contactId =
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ? AND ct.contact_id = ?
AND ct.deleted = ?
AND c.connection_id = (
SELECT cc_connection_id FROM (
SELECT
@@ -4138,7 +4161,7 @@ getContact db user@User {userId} contactId =
)
)
|]
(userId, contactId, ConnReady, ConnSndReady)
(userId, contactId, deleted, ConnReady, ConnSndReady)
getGroupChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup)
getGroupChat db user groupId pagination search_ = do
@@ -5377,7 +5400,7 @@ getXGrpMemIntroContDirect db User {userId} Contact {contactId} = do
FROM connections cc
where cc.group_member_id = mh.group_member_id
)
WHERE ct.user_id = ? AND ct.contact_id = ? AND mh.member_category = ?
WHERE ct.user_id = ? AND ct.contact_id = ? AND ct.deleted = 0 AND mh.member_category = ?
|]
(userId, contactId, GCHostMember)
where
@@ -5407,7 +5430,7 @@ getXGrpMemIntroContGroup db User {userId} GroupMember {groupMemberId} = do
FROM connections cc
where cc.group_member_id = mh.group_member_id
)
WHERE m.user_id = ? AND m.group_member_id = ? AND mh.member_category = ?
WHERE m.user_id = ? AND m.group_member_id = ? AND mh.member_category = ? AND ct.deleted = 0
|]
(userId, groupMemberId, GCHostMember)
where
+4 -4
View File
@@ -1327,7 +1327,7 @@ testUsersDifferentCIExpirationTTL tmp = do
alice #$> ("/_get chat @4 count=100", chat, [])
where
cfg = testCfg {initialCleanupManagerDelay = 0, ciExpirationInterval = 500000}
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000}
testUsersRestartCIExpiration :: HasCallStack => FilePath -> IO ()
testUsersRestartCIExpiration tmp = do
@@ -1410,7 +1410,7 @@ testUsersRestartCIExpiration tmp = do
alice #$> ("/_get chat @4 count=100", chat, [])
where
cfg = testCfg {initialCleanupManagerDelay = 0, ciExpirationInterval = 500000}
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000}
testEnableCIExpirationOnlyForOneUser :: HasCallStack => FilePath -> IO ()
testEnableCIExpirationOnlyForOneUser tmp = do
@@ -1481,7 +1481,7 @@ testEnableCIExpirationOnlyForOneUser tmp = do
-- new messages are not deleted for second user
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4"), (1, "alisa 5"), (0, "alisa 6")])
where
cfg = testCfg {initialCleanupManagerDelay = 0, ciExpirationInterval = 500000}
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000}
testDisableCIExpirationOnlyForOneUser :: HasCallStack => FilePath -> IO ()
testDisableCIExpirationOnlyForOneUser tmp = do
@@ -1539,7 +1539,7 @@ testDisableCIExpirationOnlyForOneUser tmp = do
-- second user messages are deleted
alice #$> ("/_get chat @4 count=100", chat, [])
where
cfg = testCfg {initialCleanupManagerDelay = 0, ciExpirationInterval = 500000}
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000}
testUsersTimedMessages :: HasCallStack => FilePath -> IO ()
testUsersTimedMessages tmp = do
+15 -4
View File
@@ -9,6 +9,7 @@ import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (concurrently_)
import Control.Monad (when)
import qualified Data.Text as T
import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
import Simplex.Chat.Types (GroupMemberRole (..))
import System.Directory (copyFile)
@@ -420,7 +421,7 @@ testGroup2 =
testGroupDelete :: HasCallStack => FilePath -> IO ()
testGroupDelete =
testChat3 aliceProfile bobProfile cathProfile $
testChatCfg3 cfg aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
alice ##> "/d #team"
@@ -444,12 +445,15 @@ testGroupDelete =
alice <##> bob
alice <##> cath
-- unused group contacts are deleted
threadDelay 3000000
bob ##> "@cath hi"
bob <## "no contact cath"
(cath </)
cath ##> "@bob hi"
cath <## "no contact bob"
(bob </)
where
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
testGroupSameName :: HasCallStack => FilePath -> IO ()
testGroupSameName =
@@ -1151,7 +1155,7 @@ testUpdateMemberRole =
testGroupDeleteUnusedContacts :: HasCallStack => FilePath -> IO ()
testGroupDeleteUnusedContacts =
testChat3 aliceProfile bobProfile cathProfile $
testChatCfg3 cfg aliceProfile bobProfile cathProfile $
\alice bob cath -> do
-- create group 1
createGroup3 "team" alice bob cath
@@ -1210,6 +1214,7 @@ testGroupDeleteUnusedContacts =
cath `hasContactProfiles` ["alice", "bob", "cath"]
-- delete group 2, unused contacts and profiles are deleted
deleteGroup alice bob cath "club"
threadDelay 3000000
bob ##> "/contacts"
bob <## "alice (Alice)"
bob `hasContactProfiles` ["alice", "bob"]
@@ -1217,6 +1222,7 @@ testGroupDeleteUnusedContacts =
cath <## "alice (Alice)"
cath `hasContactProfiles` ["alice", "cath"]
where
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
deleteGroup :: HasCallStack => TestCC -> TestCC -> TestCC -> String -> IO ()
deleteGroup alice bob cath group = do
alice ##> ("/d #" <> group)
@@ -1827,7 +1833,7 @@ testGroupLinkIncognitoMembership =
testGroupLinkUnusedHostContactDeleted :: HasCallStack => FilePath -> IO ()
testGroupLinkUnusedHostContactDeleted =
testChat2 aliceProfile bobProfile $
testChatCfg2 cfg aliceProfile bobProfile $
\alice bob -> do
-- create group 1
alice ##> "/g team"
@@ -1881,10 +1887,12 @@ testGroupLinkUnusedHostContactDeleted =
bob `hasContactProfiles` ["alice", "bob"]
-- delete group 2, unused host contact and profile are deleted
bobLeaveDeleteGroup alice bob "club"
threadDelay 3000000
bob ##> "/contacts"
(bob </)
bob `hasContactProfiles` ["bob"]
where
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
bobLeaveDeleteGroup :: HasCallStack => TestCC -> TestCC -> String -> IO ()
bobLeaveDeleteGroup alice bob group = do
bob ##> ("/l " <> group)
@@ -1899,7 +1907,7 @@ testGroupLinkUnusedHostContactDeleted =
testGroupLinkIncognitoUnusedHostContactsDeleted :: HasCallStack => FilePath -> IO ()
testGroupLinkIncognitoUnusedHostContactsDeleted =
testChat2 aliceProfile bobProfile $
testChatCfg2 cfg aliceProfile bobProfile $
\alice bob -> do
bob #$> ("/incognito on", id, "ok")
bobIncognitoTeam <- createGroupBobIncognito alice bob "team" "alice"
@@ -1912,15 +1920,18 @@ testGroupLinkIncognitoUnusedHostContactsDeleted =
bob `hasContactProfiles` ["alice", "alice", "bob", T.pack bobIncognitoTeam, T.pack bobIncognitoClub]
-- delete group 1, unused host contact and profile are deleted
bobLeaveDeleteGroup alice bob "team" bobIncognitoTeam
threadDelay 3000000
bob ##> "/contacts"
bob <## "i alice_1 (Alice)"
bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognitoClub]
-- delete group 2, unused host contact and profile are deleted
bobLeaveDeleteGroup alice bob "club" bobIncognitoClub
threadDelay 3000000
bob ##> "/contacts"
(bob </)
bob `hasContactProfiles` ["bob"]
where
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
createGroupBobIncognito :: HasCallStack => TestCC -> TestCC -> String -> String -> IO String
createGroupBobIncognito alice bob group bobsAliceContact = do
alice ##> ("/g " <> group)
+139 -6
View File
@@ -3,10 +3,47 @@ const markdownItAnchor = require("markdown-it-anchor")
const markdownItReplaceLink = require('markdown-it-replace-link')
const slugify = require("slugify")
const uri = require('fast-uri')
const i18n = require('eleventy-plugin-i18n');
const fs = require("fs");
const path = require("path");
const pluginRss = require('@11ty/eleventy-plugin-rss');
const i18n = require('eleventy-plugin-i18n')
const fs = require("fs")
const path = require("path")
const pluginRss = require('@11ty/eleventy-plugin-rss')
const { JSDOM } = require('jsdom')
// The implementation of Glossary feature
const md = new markdownIt()
const glossaryMarkdownContent = fs.readFileSync(path.resolve(__dirname, '../docs/GLOSSARY.md'), 'utf8')
const glossaryHtmlContent = md.render(glossaryMarkdownContent)
const glossaryDOM = new JSDOM(glossaryHtmlContent)
const glossaryDocument = glossaryDOM.window.document
const glossary = require('./src/_data/glossary.json')
glossary.forEach(item => {
const headers = Array.from(glossaryDocument.querySelectorAll("h2"))
const matchingHeader = headers.find(header => header.textContent.trim() === item.definition)
if (matchingHeader) {
let sibling = matchingHeader.nextElementSibling
let definition = ''
let firstParagraph = ''
let paragraphCount = 0
while (sibling && sibling.tagName !== 'H2') {
if (sibling.tagName === 'P') {
paragraphCount += 1
if (firstParagraph === '') {
firstParagraph = sibling.innerHTML
}
}
definition += sibling.outerHTML || sibling.textContent
sibling = sibling.nextElementSibling
}
item.definition = definition
item.tooltip = firstParagraph
item.hasMultipleParagraphs = paragraphCount > 1
}
})
const globalConfig = {
@@ -55,6 +92,102 @@ module.exports = function (ty) {
}
})
ty.addFilter('applyGlossary', function (content) {
const dom = new JSDOM(content)
const { document } = dom.window
const body = document.querySelector('body')
const allContentNodes = document.querySelectorAll('p, td, a, h1, h2, h3, h4')
const overlayIds = []
glossary.forEach((term, index) => {
let changeNoted = false
const id = term.term.toLowerCase().replace(/\s/g, '-')
allContentNodes.forEach((node) => {
const regex = new RegExp(`(?<![/#])\\b${term.term}\\b`, 'gi')
const replacement = `<span data-glossary="tooltip-${id}" class="glossary-term">${term.term}</span>`
const beforeContent = node.innerHTML
node.innerHTML = node.innerHTML.replace(regex, replacement)
if (beforeContent !== node.innerHTML && !changeNoted) {
changeNoted = true
}
})
if (changeNoted) {
const definitionTooltipDiv = document.createElement('div')
definitionTooltipDiv.id = `tooltip-${id}`
definitionTooltipDiv.className = "glossary-tooltip"
const titleH4 = document.createElement('h4')
titleH4.innerHTML = term.term
titleH4.className = "tooltip-title"
const p = document.createElement('p')
p.innerHTML = term.tooltip
const innerDiv = document.createElement('div')
innerDiv.appendChild(titleH4)
innerDiv.appendChild(p)
if (term.hasMultipleParagraphs) {
const readMoreBtn = document.createElement('button')
readMoreBtn.innerHTML = "Read more"
readMoreBtn.className = "read-more-btn open-overlay-btn"
readMoreBtn.setAttribute('data-show-overlay', id)
innerDiv.appendChild(readMoreBtn)
}
innerDiv.className = "tooltip-content"
definitionTooltipDiv.appendChild(innerDiv)
body.appendChild(definitionTooltipDiv)
}
let tooltipDom = new JSDOM(term.definition)
let tooltipDocument = tooltipDom.window.document
const hashList = [term.term.toLowerCase().replace(/\s/g, '-')]
tooltipDocument.querySelectorAll('a[href*="#"]').forEach(a => {
let hashIndex = a.href.indexOf("#")
if (hashIndex !== -1) {
let hash = a.href.substring(hashIndex + 1)
hashList.push(hash)
}
})
hashList.forEach(hash => {
if (!overlayIds.includes(hash)) {
let termFromHash = glossary.find(term => term.term.toLowerCase().replace(/\s/g, '-') === hash)
if (!termFromHash) return
const overlayDiv = document.createElement('div')
overlayDiv.id = hash
overlayDiv.className = "overlay glossary-overlay hidden"
const overlayCardDiv = document.createElement('div')
overlayCardDiv.className = "overlay-card"
const overlayTitleH1 = document.createElement('h1')
overlayTitleH1.className = "overlay-title"
overlayTitleH1.innerHTML = termFromHash.term
const overlayContent = document.createElement('div')
overlayContent.className = "overlay-content"
overlayContent.innerHTML = termFromHash.definition
const crossSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg")
crossSVG.setAttribute('class', 'close-overlay-btn')
crossSVG.setAttribute('id', 'cross')
crossSVG.setAttribute('width', '16')
crossSVG.setAttribute('height', '16')
crossSVG.setAttribute('viewBox', '0 0 13 13')
crossSVG.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
const crossPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
crossPath.setAttribute('d', 'M12.7973 11.5525L7.59762 6.49833L12.7947 1.44675C13.055 1.19371 13.0658 0.771991 12.8188 0.505331C12.5718 0.238674 12.1602 0.227644 11.8999 0.480681L6.65343 5.58028L1.09979 0.182228C0.805 0.002228 0.430001 0.002228 0.135211 0.182228C-0.159579 0.362228 -0.159579 0.697228 0.135211 0.877228L5.68885 6.27528L0.4918 11.3295C0.231501 11.5825 0.220703 12.0042 0.467664 12.2709C0.714625 12.5376 1.12625 12.5486 1.38655 12.2956L6.63302 7.196L12.1867 12.5941C12.4815 12.7741 12.8565 12.7741 13.1513 12.5941C13.4461 12.4141 13.4461 12.0791 13.1513 11.8991L12.7973 11.5525Z')
crossSVG.appendChild(crossPath)
overlayCardDiv.appendChild(overlayTitleH1)
overlayCardDiv.appendChild(overlayContent)
overlayCardDiv.appendChild(crossSVG)
overlayDiv.appendChild(overlayCardDiv)
body.appendChild(overlayDiv)
overlayIds.push(hash)
}
})
})
return dom.serialize()
})
ty.addShortcode("completeRoute", (obj) => {
const urlParts = obj.url.split("/")
@@ -88,7 +221,7 @@ module.exports = function (ty) {
}
})
ty.addPlugin(pluginRss);
ty.addPlugin(pluginRss)
ty.addPlugin(i18n, {
translations,
@@ -139,7 +272,7 @@ module.exports = function (ty) {
const url = doc.url.replace("/docs/", "")
const urlParts = url.split("/")
if (doc.inputPath.includes(referenceSubmenu)) {
if (doc.inputPath.split('/').includes(referenceSubmenu)) {
if (urlParts.length === 1 && urlParts[0] !== "") {
const index = newDocs.findIndex((ele) => ele.lang === 'en' && ele.menu === referenceMenu.menu)
if (index !== -1) {
+2 -1
View File
@@ -231,5 +231,6 @@
"click-to-see": "Click to see",
"menu": "Menu",
"on-this-page": "On this page",
"back-to-top": "Back to top"
"back-to-top": "Back to top",
"glossary": "Glossary"
}
+4 -1
View File
@@ -29,6 +29,9 @@
},
"dependencies": {
"eleventy-plugin-i18n": "^0.1.3",
"gray-matter": "^4.0.3"
"fs": "^0.0.1-security",
"gray-matter": "^4.0.3",
"jsdom": "^22.1.0",
"markdown-it": "^13.0.1"
}
}
+110
View File
@@ -0,0 +1,110 @@
[
{
"term": "Address portability",
"definition": "Address portability"
},
{
"term": "Anonymous credentials",
"definition": "Anonymous credentials"
},
{
"term": "Blockchain",
"definition": "Blockchain"
},
{
"term": "Break-in recovery",
"definition": "Post-compromise security"
},
{
"term": "Centralized network",
"definition": "Centralized network"
},
{
"term": "Content padding",
"definition": "Message padding"
},
{
"term": "Decentralized network",
"definition": "Decentralized network"
},
{
"term": "Defense in depth",
"definition": "Defense in depth"
},
{
"term": "Double ratchet algorithm",
"definition": "Double ratchet algorithm"
},
{
"term": "End-to-end encryption",
"definition": "End-to-end encryption"
},
{
"term": "Federated network",
"definition": "Federated network"
},
{
"term": "Forward secrecy",
"definition": "Forward secrecy"
},
{
"term": "Key agreement protocol",
"definition": "Key agreement protocol"
},
{
"term": "Key exchange",
"definition": "Key agreement protocol"
},
{
"term": "Man-in-the-middle attack",
"definition": "Man-in-the-middle attack"
},
{
"term": "Merkle directed acyclic graph",
"definition": "Merkle directed acyclic graph"
},
{
"term": "Message padding",
"definition": "Message padding"
},
{
"term": "Onion routing",
"definition": "Onion routing"
},
{
"term": "Overlay network",
"definition": "Overlay network"
},
{
"term": "Pairwise pseudonymous identifier",
"definition": "Pairwise pseudonymous identifier"
},
{
"term": "Peer-to-peer",
"definition": "Peer-to-peer"
},
{
"term": "Perfect forward secrecy",
"definition": "Forward secrecy"
},
{
"term": "Post-compromise security",
"definition": "Post-compromise security"
},
{
"term": "Post-quantum cryptography",
"definition": "Post-quantum cryptography"
},
{
"term": "Proxied peer-to-peer",
"definition": "Proxied peer-to-peer"
},
{
"term": "Recovery from compromise",
"definition": "Post-compromise security"
},
{
"term": "User identity",
"definition": "User identity"
}
]
+1 -1
View File
@@ -22,7 +22,7 @@
{% endif %}
</div>
<svg class="fill-grey-black dark:fill-white fixed right-5 top-5 cursor-pointer close-overlay-btn" id="cross" width="16" height="16" viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg">
<svg class="close-overlay-btn" id="cross" width="16" height="16" viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg">
<path d="M12.7973 11.5525L7.59762 6.49833L12.7947 1.44675C13.055 1.19371 13.0658 0.771991 12.8188 0.505331C12.5718 0.238674 12.1602 0.227644 11.8999 0.480681L6.65343 5.58028L1.09979 0.182228C0.839522 -0.070157 0.427909 -0.059127 0.18094 0.207531C-0.0660305 0.474191 -0.0552645 0.895911 0.205003 1.14894L5.70862 6.49833L0.20247 11.851C-0.0577975 12.104 -0.0685635 12.5257 0.178407 12.7924C0.306324 12.9306 0.477936 13 0.650181 13C0.811033 13 0.971873 12.9397 1.09726 12.817L6.65343 7.41639L11.9025 12.5186C12.0285 12.6406 12.1893 12.7015 12.3495 12.7015C12.5218 12.7015 12.6934 12.6321 12.8213 12.4939C13.0689 12.2273 13.0582 11.8062 12.7973 11.5525Z"/>
</svg>
</div>
+6 -6
View File
@@ -7,8 +7,8 @@
<section class="hidden xl:block h-screen pt-[66px] bg-white dark:bg-gradient-radial-mobile dark:lg:bg-gradient-radial">
<div class="container m-auto h-full flex items-center justify-between px-5">
<div class="flex flex-col items-start justify-center w-full h-full">
<p class="text-[38px] leading-[43px] font-bold max-w-[500px] mb-[30px] primary-header-contact">{{ header | i18n({}, lang ) | safe }}</p>
<p class="text-[20px] leading-[28px] text-[#606C71] dark:text-white font-bold max-w-[475px] mb-[80px] secondary-header-contact">{{ "contact-hero-subheader" | i18n({}, lang ) | safe }}</p>
<h1 class="text-[38px] leading-[43px] font-bold max-w-[500px] mb-[30px] primary-header-contact">{{ header | i18n({}, lang ) | safe }}</h1>
<h2 class="text-[20px] leading-[28px] text-[#606C71] dark:text-white font-bold max-w-[475px] mb-[80px] secondary-header-contact">{{ "contact-hero-subheader" | i18n({}, lang ) | safe }}</h2>
<p class="text-grey-black dark:text-white text-base mb-[16px]">
{{ "contact-hero-p-1" | i18n({}, lang ) | safe }}
</p>
@@ -43,7 +43,7 @@
<section class="block xl:hidden pt-[106px] pb-[90px] bg-white dark:bg-gradient-radial-mobile dark:lg:bg-gradient-radial">
<div class="container m-auto h-full px-5">
<div class="flex flex-col items-center">
<p class="text-[28px] font-bold text-center max-w-[602px] mb-[40px] primary-header-contact">{{ header | i18n({}, lang ) | safe }}</p>
<h1 class="text-[28px] font-bold text-center max-w-[602px] mb-[40px] primary-header-contact">{{ header | i18n({}, lang ) | safe }}</h1>
<p class="text-[20px] leading-[28px] text-grey-black dark:text-white font-medium mb-[30px]">{{ "to-make-a-connection" | i18n({}, lang ) | safe }}</p>
<div class="flex flex-col justify-center items-center p-4 w-full max-w-[468px] min-h-[131px] rounded-[30px] border-[1px] border-[#A8B0B4] dark:border-white border-opacity-60 mb-6 relative">
@@ -88,7 +88,7 @@
<div class="hidden md:block xl:hidden for-tablet">
<div class="contact-tab">
<div class="flex items-center justify-between my-[40px] contact-tab-btn cursor-pointer">
<p class="text-xl font-bold">{{ "scan-the-qr-code-with-the-simplex-chat-app" | i18n({}, lang ) | safe }}</p>
<h2 class="text-xl font-bold">{{ "scan-the-qr-code-with-the-simplex-chat-app" | i18n({}, lang ) | safe }}</h2>
<svg class="fill-grey-black dark:fill-white" width="10" height="5" viewBox="0 0 10 5" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.40813 4.79332C8.69689 5.06889 9.16507 5.06889 9.45384 4.79332C9.7426 4.51775 9.7426 4.07097 9.45384 3.7954L5.69327 0.206676C5.65717 0.17223 5.61827 0.142089 5.57727 0.116255C5.29026 -0.064587 4.90023 -0.0344467 4.64756 0.206676L0.886983 3.7954C0.598219 4.07097 0.598219 4.51775 0.886983 4.79332C1.17575 5.06889 1.64393 5.06889 1.93269 4.79332L5.17041 1.70356L8.40813 4.79332Z"/>
</svg>
@@ -108,7 +108,7 @@
<div class="hidden xl:block">
<div class="contact-tab">
<div class="flex items-center justify-between my-[40px] contact-tab-btn cursor-pointer">
<p class="text-xl font-bold">{{ "installing-simplex-chat-to-terminal" | i18n({}, lang ) | safe }}</p>
<h2 class="text-xl font-bold">{{ "installing-simplex-chat-to-terminal" | i18n({}, lang ) | safe }}</h2>
<svg class="fill-grey-black dark:fill-white" width="10" height="5" viewBox="0 0 10 5" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.40813 4.79332C8.69689 5.06889 9.16507 5.06889 9.45384 4.79332C9.7426 4.51775 9.7426 4.07097 9.45384 3.7954L5.69327 0.206676C5.65717 0.17223 5.61827 0.142089 5.57727 0.116255C5.29026 -0.064587 4.90023 -0.0344467 4.64756 0.206676L0.886983 3.7954C0.598219 4.07097 0.598219 4.51775 0.886983 4.79332C1.17575 5.06889 1.64393 5.06889 1.93269 4.79332L5.17041 1.70356L8.40813 4.79332Z"/>
</svg>
@@ -139,7 +139,7 @@
<div class="contact-tab">
<div class="flex items-center justify-between my-[40px] contact-tab-btn cursor-pointer">
<p class="text-xl font-bold">{{ "if-you-already-installed-simplex-chat-for-the-terminal" | i18n({}, lang ) | safe }}</p>
<h2 class="text-xl font-bold">{{ "if-you-already-installed-simplex-chat-for-the-terminal" | i18n({}, lang ) | safe }}</h2>
<svg class="fill-grey-black dark:fill-white" width="10" height="5" viewBox="0 0 10 5" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.40813 4.79332C8.69689 5.06889 9.16507 5.06889 9.45384 4.79332C9.7426 4.51775 9.7426 4.07097 9.45384 3.7954L5.69327 0.206676C5.65717 0.17223 5.61827 0.142089 5.57727 0.116255C5.29026 -0.064587 4.90023 -0.0344467 4.64756 0.206676L0.886983 3.7954C0.598219 4.07097 0.598219 4.51775 0.886983 4.79332C1.17575 5.06889 1.64393 5.06889 1.93269 4.79332L5.17041 1.70356L8.40813 4.79332Z"/>
</svg>
+1 -1
View File
@@ -54,7 +54,7 @@
<div class="h-[40px] flex gap-4 justify-center">
<a href="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">
<svg class="fill-primary-light dark:fill-primary-dark" width="40" height="40" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.585 0C11.9025 0 0 11.9025 0 26.585C0 41.2674 11.9025 53.1699 26.585 53.1699C41.2674 53.1699 53.1699 41.2674 53.1699 26.585C53.1699 11.9025 41.2674 0 26.585 0ZM11.3862 17.3518L17.6366 23.4373L23.9313 17.3088L17.6787 11.2209L20.866 8.1179L27.1187 14.2061L33.4932 8L36.6199 11.044L30.2448 17.25L36.4982 23.3379L42.8733 17.1321L46 20.1761L39.6249 26.3818L45.8789 32.4702L42.6916 35.5732L36.4376 29.4848L30.0631 35.6906L36.3171 41.7791L33.1299 44.8821L26.8759 38.7936L20.5026 45L17.3759 41.956L23.8003 35.693L17.5493 29.6073L11.1255 35.8621L8 32.8194L14.4244 26.5646L8.17397 20.4792L11.3862 17.3518ZM27.0125 32.5656L33.311 26.4408L27.0576 20.3535L27.0568 20.3516L20.7615 26.4799L27.0125 32.5656Z" fill="#0053D0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.585 0C11.9025 0 0 11.9025 0 26.585C0 41.2674 11.9025 53.1699 26.585 53.1699C41.2674 53.1699 53.1699 41.2674 53.1699 26.585C53.1699 11.9025 41.2674 0 26.585 0ZM11.3862 17.3518L17.6366 23.4373L23.9313 17.3088L17.6787 11.2209L20.866 8.1179L27.1187 14.2061L33.4932 8L36.6199 11.044L30.2448 17.25L36.4982 23.3379L42.8733 17.1321L46 20.1761L39.6249 26.3818L45.8789 32.4702L42.6916 35.5732L36.4376 29.4848L30.0631 35.6906L36.3171 41.7791L33.1299 44.8821L26.8759 38.7936L20.5026 45L17.3759 41.956L23.8003 35.693L17.5493 29.6073L11.1255 35.8621L8 32.8194L14.4244 26.5646L8.17397 20.4792L11.3862 17.3518ZM27.0125 32.5656L33.311 26.4408L27.0576 20.3535L27.0568 20.3516L20.7615 26.4799L27.0125 32.5656Z"/>
</svg>
</a>
<a href="https://github.com/simplex-chat" target="_blank">
+4 -4
View File
@@ -7,8 +7,8 @@
<img class="static-phone-mobile md:hidden" src="/img/new/mobile-hero.png" alt="" />
<article class="w-full xl:max-w-[600px] landing-page-header-article">
<p class="primary-header text-center xl:text-left xl:rtl:text-right font-bold text-[38px] md:text-[55px] leading-[46px] md:leading-[63px] mb-2 xl:mb-8">{{ "hero-header" | i18n({}, lang ) | safe }}</p>
<p class="secondary-header text-center xl:text-left xl:rtl:text-right font-bold text-[28px] md:text-[38px] leading-[36px] md:leading-[43px] mb-2 xl:mb-8 tracking-[0.01em]">{{ "hero-subheader" | i18n({}, lang ) | safe }}</p>
<h1 class="primary-header text-center xl:text-left xl:rtl:text-right font-bold text-[38px] md:text-[55px] leading-[46px] md:leading-[63px] mb-2 xl:mb-8">{{ "hero-header" | i18n({}, lang ) | safe }}</h1>
<h2 class="secondary-header text-center xl:text-left xl:rtl:text-right font-bold text-[28px] md:text-[38px] leading-[36px] md:leading-[43px] mb-2 xl:mb-8 tracking-[0.01em]">{{ "hero-subheader" | i18n({}, lang ) | safe }}</h2>
<p class="landing-page-header-article-paragraph text-black dark:text-white text-center xl:text-justify text-[16px] leading-[24px] mb-[20px] header-description">
{{ "hero-p-1" | i18n({}, lang ) | safe }}
</p>
@@ -87,9 +87,9 @@
</div>
<article class="w-full xl:max-w-[600px] landing-page-header-article px-5">
<p class="text-active-blue text-center xl:text-left xl:rtl:text-right font-bold text-[28px] md:text-[35px] leading-[36px] md:leading-[43px] mb-[28px]">
<h2 class="text-active-blue text-center xl:text-left xl:rtl:text-right font-bold text-[28px] md:text-[35px] leading-[36px] md:leading-[43px] mb-[28px]">
{{ "hero-2-header" | i18n({}, lang ) | safe }}
</p>
</h2>
<p class="text-center text-black dark:text-white xl:text-justify leading-[24px] text-[16px] mb-10 xl:mb-[25px] header-description">
{{ "hero-2-header-desc" | i18n({}, lang ) | safe }}
</p>
+1 -1
View File
@@ -38,7 +38,7 @@
{% include "navbar.html" %}
</section>
{{ content | safe }}
{{ content | applyGlossary | safe }}
{% include "footer.html" %}
+2
View File
@@ -67,6 +67,8 @@
</a></li>
<li><a href="/docs/protocol/simplex-chat.html" class="lg:px-[20px] inline-block"
>{{ "chat-protocol" | i18n({}, lang ) | safe }}</a></li>
<li><a href="/docs/glossary.html" class="lg:px-[20px] inline-block"
>{{ "glossary" | i18n({}, lang ) | safe }}</a></li>
<hr class=" h-[1px] w-full dark:opacity-[0.1]">
@@ -1,7 +1,7 @@
{# join simplex #}
<section id="join-simplex" class="bg-primary-bg-light dark:bg-primary-bg-dark lg:h-[855px] py-[90px] px-5">
<div class="container flex flex-col items-center">
<p class="text-[38px] leading-[36px] md:leading-[55px] text-grey-black dark:text-white text-center font-bold mb-5"><span class="text-active-blue">{{ "join" | i18n({}, lang ) | safe }}</span> SimpleX</p>
<h2 class="text-[38px] leading-[36px] md:leading-[55px] text-grey-black dark:text-white text-center font-bold mb-5"><span class="text-active-blue">{{ "join" | i18n({}, lang ) | safe }}</span> SimpleX</h2>
<p class="text-black dark:text-white text-base text-center mb-14">{{ "we-invite-you-to-join-the-conversation" | i18n({}, lang ) | safe }}</p>
<div class="flex flex-col items-center gap-5 self-stretch mb-12">
@@ -1,6 +1,6 @@
<section id="why-simplex" class="bg-primary-bg-light dark:bg-primary-bg-dark py-[90px] overflow-hidden px-0 sm:px-1 xl:h-[888px]">
<div class="container scale-100">
<p class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-14 px-5 sm:px-4">{{ "why-simplex-is" | i18n({}, lang ) | safe }} <span class="gradient-text">{{ "unique" | i18n({}, lang ) | safe }}</span></p>
<h2 class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-14 px-5 sm:px-4">{{ "why-simplex-is" | i18n({}, lang ) | safe }} <span class="gradient-text">{{ "unique" | i18n({}, lang ) | safe }}</span></h2>
<div class="swiper unique-swiper px-5 sm:px-4 py-2">
<div class="swiper-wrapper mb-16">
@@ -15,8 +15,8 @@
<div class="card-content absolute md:static px-4 md:px-0 bottom-[80px] right-1 left-1 h-[180px] md:h-fit pt-5 lg:pt-0 bg-primary-bg-light dark:bg-primary-bg-dark">
<div class="content-head">
<p class="text-[35px] lg:text-[65px] font-bold tracking-[0.06em] text-active-blue text-center md:text-left md:rtl:text-right">#{{ section.id }}</p>
<p class="w-full max-w-[617px] text-[25px] leading-[33px] lg:text-[35px] lg:leading-[45px] text-center md:text-left md:rtl:text-right font-bold text-grey-black dark:text-white">{{ section.title | i18n({}, lang ) | safe }}</p>
<h1 class="text-[35px] lg:text-[65px] font-bold tracking-[0.06em] text-active-blue text-center md:text-left md:rtl:text-right">#{{ section.id }}</h1>
<h3 class="w-full max-w-[617px] text-[25px] leading-[33px] lg:text-[35px] lg:leading-[45px] text-center md:text-left md:rtl:text-right font-bold text-grey-black dark:text-white">{{ section.title | i18n({}, lang ) | safe }}</h3>
</div>
<div class="content-body py-5 md:py-7">
+1 -1
View File
@@ -25,7 +25,7 @@
<section id="simplex-explained" class="bg-primary-bg-light dark:bg-primary-bg-dark lg:h-[890px] py-[90px] px-5">
<div class="container">
<p class="text-[35px] leading-[45px] md:leading-[55px] lg:text-[38px] text-center font-bold text-grey-black dark:text-white mb-9">{{ "simplex-explained" | i18n({}, lang ) | safe }}</p>
<h2 class="text-[35px] leading-[45px] md:leading-[55px] lg:text-[38px] text-center font-bold text-grey-black dark:text-white mb-9">{{ "simplex-explained" | i18n({}, lang ) | safe }}</h2>
<!-- Tab links -->
<div class="tabs hidden md:flex gap-2 mb-24">
+1 -1
View File
@@ -35,7 +35,7 @@ active_blog: true
<section class="py-10 px-5 mt-[66px]" id="blog-list">
<div class="container">
<p class="text-[38px] text-center font-bold text-active-blue mb-9">Latest news</p>
<h1 class="text-[38px] text-center font-bold text-active-blue mb-9">Latest news</h1>
{% for blog in collections.blogs %}
{% if not(blog.data.draft) %}
+232 -6
View File
@@ -428,13 +428,13 @@ header nav {
transform: translateY(40px);
}
.card:not(.no-hover):hover > div:nth-child(1) {
.card:not(.no-hover):hover > div:nth-child(1),
.card.hovered > div:nth-child(1) {
height: 200px;
padding: 12px 10px;
}
.card:not(.no-hover):hover > div:nth-child(2) {
.card:not(.no-hover):hover > div:nth-child(2),
.card.hovered > div:nth-child(2) {
height: 270px;
padding: 8px 24px;
}
.card.card-active > div:nth-child(2) {
height: 480px;
@@ -443,13 +443,16 @@ header nav {
.card:not(.no-hover):hover > div:nth-child(2) > *:nth-child(2),
.card:not(.no-hover):hover > div:nth-child(2) > *:nth-child(3),
.card.card-active > div:nth-child(2) > *:nth-child(2),
.card.card-active > div:nth-child(2) > *:nth-child(3) {
.card.card-active > div:nth-child(2) > *:nth-child(3),
.card:not(.no-hover).hovered > div:nth-child(2) > *:nth-child(2),
.card:not(.no-hover).hovered > div:nth-child(2) > *:nth-child(3) {
opacity: 1;
max-height: 480px;
transform: translateY(0px);
}
.card:not(.no-hover):hover > div:nth-child(2) > *:nth-child(3){
.card:not(.no-hover):hover > div:nth-child(2) > *:nth-child(3),
.card:not(.no-hover).hovered > div:nth-child(2) > *:nth-child(3) {
opacity: 0;
}
@@ -710,4 +713,227 @@ p a{
.contact-tab.active svg,
.contact-tab:hover svg{
transform: rotate(0deg);
}
/* Glossary */
.glossary-term{
display: inline-block;
text-decoration: underline;
text-decoration-style: dashed;
text-underline-offset: 3px;
position: relative;
z-index: 10000;
}
.glossary-term::before{
content: '';
position: absolute;
top: -5px;
bottom: -5px;
left: 0;
right: 0;
opacity: 0;
z-index: 9;
}
.glossary-tooltip {
visibility: hidden;
opacity: 0;
position: fixed;
width: 320px;
max-width: 100%;
z-index: 10001;
transition: opacity .5s;
padding-left: 5px;
padding-right: 5px;
}
.glossary-tooltip .tooltip-content{
background-color: #fff;
color: #000;
text-align: left;
padding: .8rem 1.2rem;
border-radius: 7px;
font-size: 14px;
line-height: 1.3rem;
box-shadow: 0 5px 10px rgba(0,0,0,.08);
}
.dark .glossary-tooltip .tooltip-content{
background-color: #000;
color: #fff;
box-shadow: none;
}
.tooltip-title{
margin-bottom: 0.5rem;
color: #0197FF;
font-weight: 600;
font-size: 1.1rem;
}
.dark .tooltip-title{
color: #70F0F9;
}
.glossary-tooltip .read-more-btn{
color: #0053D0;
display: block;
text-decoration: underline;
margin-top: .8rem;
text-underline-offset: 2px;
}
.dark .glossary-tooltip .read-more-btn{
color: #70F0F9;
}
.glossary-overlay{
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: transparent;
align-items: center;
justify-content: center;
padding: 0.75rem;
z-index: 10005;
}
@media screen and (min-width: 768px) {
.glossary-overlay{
padding: 2.5rem;
}
}
.glossary-overlay::before {
content: '';
position: absolute;
width: 100%;
height: 100%;
background-color:#F3F6F7;
opacity: 0.9;
}
.dark .glossary-overlay::before {
content: '';
background-color: #0C0B13;
}
.glossary-overlay .overlay-card{
width: 100%;
background-color: white;
opacity: 1;
height: 100%;
z-index: 10006;
border-radius: 0.375rem;
box-shadow: 0px 3px 12px rgba(0, 0, 0, 0.2);
padding: 2.5rem 1.5rem;
overflow: auto;
--tw-scale-x: 1;
--tw-scale-y: 1;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.dark .glossary-overlay .overlay-card{
background-color: #17203D;
}
@media (min-width: 640px) {
.glossary-overlay .overlay-card{
padding: 3.5rem;
}
}
@media (min-width: 768px) {
.glossary-overlay .overlay-card{
width: fit-content;
max-width: 558px;
height: fit-content;
max-height: 660px;
}
}
.glossary-overlay .overlay-card .overlay-title{
font-size: 1.875rem;
line-height: 2.25rem;
font-weight: 700;
margin-bottom: 1rem;
margin-bottom: 1.5rem;
--tw-text-opacity: 1;
color: rgb(1 151 255 / var(--tw-text-opacity));
}
.glossary-overlay .overlay-card .overlay-content{
display: flex;
flex-direction: column;
gap: 1.5rem;
font-size: 1rem;
line-height: 1.5rem;
color: #3F484B;
}
.dark .glossary-overlay .overlay-card .overlay-content{
color: #fff;
}
.close-overlay-btn{
fill: #3F484B;
position: fixed;
right: 1.25rem;
top: 1.25rem;
cursor: pointer;
}
.dark .close-overlay-btn{
fill: #fff;
}
.glossary-overlay ul {
overflow-x: auto;
}
.glossary-overlay ul,
.glossary-overlay ol {
list-style-position: inside;
overflow: auto;
margin: 1rem 0;
/* padding-left: 1rem; */
}
.glossary-overlay ul li,
.glossary-overlay ol li {
margin-right: 0;
margin-left: 0;
-webkit-margin-start: 1.1rem;
color: #000;
}
.dark .glossary-overlay ul li,
.dark .glossary-overlay ol li {
color: #fff;
}
.glossary-overlay ul li::marker,
.glossary-overlay ol li::marker {
color: black;
}
.dark .glossary-overlay ul li::marker,
.dark .glossary-overlay ol li::marker {
color: white;
}
.glossary-overlay ul li a,
.glossary-overlay ol li a {
line-height: 30px;
}
.glossary-overlay ul li {
list-style: disc;
}
.glossary-overlay ol li {
list-style: decimal;
}
+10 -10
View File
@@ -11,7 +11,7 @@ active_home: true
<section id="why-privacy" class="bg-secondary-bg-light dark:bg-secondary-bg-dark py-[90px] px-5 lg:h-[888px]">
<div class="container">
<p class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-4 md:mb-8">{{ "privacy-matters-section-header" | i18n({}, lang ) | safe }}</p>
<h2 class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-4 md:mb-8">{{ "privacy-matters-section-header" | i18n({}, lang ) | safe }}</h2>
<p class="text-center text-[18px] md:text-[20px] font-medium mb-7 md:mb-16 lg:mb-20 text-black dark:text-white">{{ "privacy-matters-section-subheader" | i18n({}, lang ) | safe }}</p>
<div class="flex flex-col lg:flex-row gap-[20px] mb-[62px] lg:mb-[90px]">
@@ -41,7 +41,7 @@ active_home: true
{# Features #}
<section id="features" class="bg-secondary-bg-light dark:bg-secondary-bg-dark py-[95px] px-5 lg:h-[888px]">
<div class="container">
<p class="text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold gradient-text mb-20">{{ "features" | i18n({}, lang ) | safe }}</p>
<h2 class="text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold gradient-text mb-20">{{ "features" | i18n({}, lang ) | safe }}</h2>
<div class="mb-[50px] grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-x-10 gap-y-32">
{% for feature in features.sections %}
@@ -60,7 +60,7 @@ active_home: true
{# what makes simplex private #}
<section id="privacy" class="bg-primary-bg-light dark:bg-primary-bg-dark py-[90px] overflow-hidden px-5 lg:h-[888px]">
<div class="container scale-100">
<p class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-20">{{ "simplex-private-section-header" | i18n({}, lang ) | safe }}</p>
<h2 class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-20">{{ "simplex-private-section-header" | i18n({}, lang ) | safe }}</h2>
<div class="swiper private-swiper overflow-hidden px-4 py-2">
<div class="swiper-wrapper mb-16">
@@ -71,8 +71,8 @@ active_home: true
<img class="w-full max-w-[223px] h-full max-h-[226px] dark:hidden" src="{{ section.imgLight }}" alt=""/>
<img class="w-full max-w-[223px] h-full max-h-[226px] hidden dark:block" src="{{ section.imgDark }}" alt=""/>
</div>
<div class="flex flex-col items-center justify-between h-[138px] absolute bottom-0 py-6 px-6 bg-card-desc-bg-light dark:bg-card-desc-bg-dark rounded-b-[20px]">
<p class="text-grey-black dark:text-white text-[18px] my-4 font-bold leading-[26px] tracking-[0.01em] text-center">{{ section.title | i18n({}, lang ) | safe }}</p>
<div class="card-content flex flex-col items-center justify-between h-[138px] absolute bottom-0 py-6 px-6 bg-card-desc-bg-light dark:bg-card-desc-bg-dark rounded-b-[20px]">
<h3 class="text-grey-black dark:text-white text-[18px] my-4 font-bold leading-[26px] tracking-[0.01em] text-center">{{ section.title | i18n({}, lang ) | safe }}</h3>
<div class="flex-1 py-3 flex flex-col gap-3">
{% for point in section.points %}
<p class="text-grey-black dark:text-white text-[14px] text-center">{{ point | i18n({}, lang ) | safe }}</p>
@@ -102,7 +102,7 @@ active_home: true
{# Network #}
<section id="network" class="bg-secondary-bg-light dark:bg-secondary-bg-dark lg:h-[642px] py-[95px] px-5">
<div class="container">
<p class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-5">{{ "simplex-network-section-header" | i18n({}, lang ) | safe }}</p>
<h2 class="text-grey-black dark:text-white text-[35px] leading-[45px] md:leading-[55px] lg:text-[45px] text-center font-bold mb-5">{{ "simplex-network-section-header" | i18n({}, lang ) | safe }}</h2>
<p class="text-black dark:text-white text-[16px] font-normal text-center mb-16">{{ "simplex-network-section-desc" | i18n({}, lang ) | safe }}</p>
<div class="flex flex-col lg:flex-row justify-between gap-12 md:gap-14 lg:gap-16">
@@ -112,7 +112,7 @@ active_home: true
<img src="/img/new/network-1-dark.svg" alt="" class="hidden dark:block"/>
</div>
<div class="md:flex-[2] flex flex-col items-center justify-center">
<p class="text-active-blue text-xl font-bold text-center md:text-left lg:text-center self-stretch">{{ "simplex-network-1-header" | i18n({}, lang ) | safe }}</p>
<h3 class="text-active-blue text-xl font-bold text-center md:text-left lg:text-center self-stretch">{{ "simplex-network-1-header" | i18n({}, lang ) | safe }}</h3>
<p class="text-black dark:text-white text-base font-normal text-center md:text-left lg:text-center">
{{ "simplex-network-1-desc" | i18n({}, lang ) | safe }} <a href="javascript:void(0)" data-show-overlay="{{ simplex_network_overlay.sections[0].overlayContent.overlayId }}" class="open-overlay-btn">{{ "simplex-network-1-overlay-linktext" | i18n({}, lang ) | safe }}</a>.
{{ overlay(simplex_network_overlay.sections[0],lang) }}
@@ -128,7 +128,7 @@ active_home: true
<img src="/img/new/network-2-dark.svg" alt="" class="hidden dark:block"/>
</div>
<div class="md:flex-[2] flex flex-col items-center justify-center">
<p class="text-active-blue text-xl font-bold text-center md:text-left lg:text-center self-stretch">{{ "simplex-network-2-header" | i18n({}, lang ) | safe }}</p>
<h3 class="text-active-blue text-xl font-bold text-center md:text-left lg:text-center self-stretch">{{ "simplex-network-2-header" | i18n({}, lang ) | safe }}</h3>
<p class="text-black dark:text-white text-base font-normal text-center md:text-left lg:text-center">
{{ "simplex-network-2-desc" | i18n({}, lang ) | safe }}
</p>
@@ -143,7 +143,7 @@ active_home: true
<img src="/img/new/network-3-dark.svg" alt="" class="hidden dark:block"/>
</div>
<div class="md:flex-[2] flex flex-col items-center justify-center">
<p class="text-active-blue text-xl font-bold text-center md:text-left lg:text-center self-stretch">{{ "simplex-network-3-header" | i18n({}, lang ) | safe }}</p>
<h3 class="text-active-blue text-xl font-bold text-center md:text-left lg:text-center self-stretch">{{ "simplex-network-3-header" | i18n({}, lang ) | safe }}</h3>
<p class="text-black dark:text-white text-base font-normal text-center md:text-left lg:text-center">
{{ "simplex-network-3-desc" | i18n({}, lang ) | safe }}
</p>
@@ -163,7 +163,7 @@ active_home: true
{# Comparison #}
<section id="comparison" class="bg-secondary-bg-light dark:bg-secondary-bg-dark lg:h-[950px] py-[90px] px-5">
<div class="text-grey-black dark:text-white container flex flex-col">
<p class="text-[35px] leading-[43px] md:leading-[55px] lg:leading-[36px] text-center font-bold mb-12 lg:mb-[90px]">{{ "comparison-section-header" | i18n({}, lang ) | safe }}</p>
<h2 class="text-[35px] leading-[43px] md:leading-[55px] lg:leading-[36px] text-center font-bold mb-12 lg:mb-[90px]">{{ "comparison-section-header" | i18n({}, lang ) | safe }}</h2>
<div class="w-full overflow-auto">
<table class="w-full border-separate border-spacing-x-5 border-spacing-y-2 mb-14">
+139 -2
View File
@@ -65,6 +65,19 @@ const privateSwiper = new Swiper('.private-swiper', {
allowTouchMove: true,
}
},
on: {
slideChange: function () {
const privateSwiperGlossaryTerms = document.querySelectorAll('.private-swiper .glossary-term');
privateSwiperGlossaryTerms.forEach(function (glossaryTerm) {
var tooltipId = glossaryTerm.getAttribute('data-glossary');
var tooltip = document.getElementById(tooltipId);
tooltip.style.visibility = 'hidden';
tooltip.style.opacity = '0';
const privateSwiper = glossaryTerm.closest('.private-swiper')
if (privateSwiper) glossaryTerm.closest('.card').classList.remove('hovered');
})
}
}
});
const simplexExplainedSwiper = new Swiper(".simplex-explained-swiper", {
@@ -86,6 +99,17 @@ const simplexExplainedSwiper = new Swiper(".simplex-explained-swiper", {
pagination: {
el: ".simplex-explained-swiper-pagination",
clickable: true
},
on: {
slideChange: function () {
const explainedSwiperGlossaryTerms = document.querySelectorAll('.simplex-explained-swiper .glossary-term');
explainedSwiperGlossaryTerms.forEach(function (glossaryTerm) {
var tooltipId = glossaryTerm.getAttribute('data-glossary');
var tooltip = document.getElementById(tooltipId);
tooltip.style.visibility = 'hidden';
tooltip.style.opacity = '0';
})
}
}
});
@@ -163,6 +187,13 @@ function openOverlay() {
const scrollToEl = document.getElementById(scrollTo)
if (scrollToEl) scrollToEl.scrollIntoView(true)
}
const currentOpenedGlossaryOverlay = document.querySelector('.glossary-overlay.flex')
if (currentOpenedGlossaryOverlay) {
currentOpenedGlossaryOverlay.classList.remove('flex')
currentOpenedGlossaryOverlay.classList.add('hidden')
}
el.classList.remove('hidden')
el.classList.add('flex')
document.body.classList.add('lock-scroll')
@@ -170,5 +201,111 @@ function openOverlay() {
}
}
window.addEventListener('load', openOverlay);
window.addEventListener('hashchange', openOverlay);
function updatePointerEventsInPrivateSwiperCards() {
var privateSwiperCards = document.querySelectorAll('.private-swiper .card')
privateSwiperCards.forEach(function (card) {
var cardContent = card.querySelector('.card-content')
function updatePointerEvents() {
var cardContentGlossaryTerms = cardContent.querySelectorAll('.glossary-term')
cardContentGlossaryTerms.forEach(function (glossaryTerm) {
if (cardContent.offsetHeight >= 270) {
glossaryTerm.style.pointerEvents = 'all'
} else {
glossaryTerm.style.pointerEvents = 'none'
}
})
}
updatePointerEvents()
cardContent.addEventListener('click', updatePointerEvents)
cardContent.addEventListener('mousemove', updatePointerEvents)
})
}
function updateTooltipPosition(glossaryTerm, tooltip) {
var glossaryTermOffset = glossaryTerm.getBoundingClientRect()
var tooltipOffset = tooltip.getBoundingClientRect()
if (glossaryTermOffset.top >= tooltipOffset.height) {
tooltip.style.top = glossaryTermOffset.top - tooltipOffset.height + 'px'
} else {
tooltip.style.top = glossaryTermOffset.bottom + 'px'
}
var leftPosition = glossaryTermOffset.left + glossaryTerm.offsetWidth / 2 - tooltip.offsetWidth / 2
if (leftPosition < 0) {
tooltip.style.left = '0px'
} else if (leftPosition + tooltip.offsetWidth > window.innerWidth) {
tooltip.style.left = window.innerWidth - tooltip.offsetWidth + 'px'
} else {
tooltip.style.left = leftPosition + 'px'
}
}
function setupTooltip(glossaryTerm) {
var tooltipId = glossaryTerm.getAttribute('data-glossary')
var tooltip = document.getElementById(tooltipId)
function showTooltip() {
tooltip.style.visibility = 'visible'
tooltip.style.opacity = '1'
updateTooltipPosition(glossaryTerm, tooltip)
const privateSwiper = glossaryTerm.closest('.private-swiper')
if (privateSwiper) glossaryTerm.closest('.card').classList.add('hovered')
}
function hideTooltip() {
tooltip.style.visibility = 'hidden'
tooltip.style.opacity = '0'
const privateSwiper = glossaryTerm.closest('.private-swiper')
if (privateSwiper) glossaryTerm.closest('.card').classList.remove('hovered')
if (!glossaryTerm.matches(':hover') && !tooltip.matches(':hover')) {
glossaryTerm.classList.remove('active-term')
tooltip.removeEventListener('mouseover', showTooltip)
tooltip.removeEventListener('mouseout', hideTooltip)
}
}
let click = 0
glossaryTerm.addEventListener('mouseover', () => {
glossaryTerm.classList.add('active-term')
showTooltip()
tooltip.addEventListener('mouseover', showTooltip)
tooltip.addEventListener('mouseout', hideTooltip)
})
glossaryTerm.addEventListener('mouseout', function (event) {
click = 0
hideTooltip()
})
glossaryTerm.addEventListener('click', function (event) {
event.stopPropagation()
if (click == 1) {
hideTooltip()
click = 0
}
else {
showTooltip()
click = 1
}
})
}
window.addEventListener('load', () => {
openOverlay()
updatePointerEventsInPrivateSwiperCards()
window.addEventListener('scroll', function () {
let activeTerm = document.querySelector('.active-term')
if (activeTerm) {
var tooltipId = activeTerm.getAttribute('data-glossary')
var tooltip = document.getElementById(tooltipId)
updateTooltipPosition(activeTerm, tooltip)
}
})
const glossaryTerms = document.querySelectorAll('.glossary-term')
glossaryTerms.forEach(setupTooltip)
})
window.addEventListener('hashchange', openOverlay)