mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-28 09:59:44 +00:00
website: more sections (#6289)
* upload assets & change font to gt-walsheim * fix language issue in learn-more page * design cover for desktop * add section 2, 3 & 4 for desktop * website: directory page (#6283) * website: directory page * core: use markdown in directory entries * render markdown on directory page * update markdown * toggle secrets on click * update listings asynchronously * add group links to the listing * cleanup * better directory layout with pagination * script to run website * update page navigation * search * readable markdown colors, better "read less" * core: atomic update of directory listings, to avoid files unavailable * fix symlink, sort entries on page with new first * update listings every 15 min, add activeAt time * fix sorting in the page and listing url * replace simplex:/ links on desktop * website: fix search in directory * add section 5 for Desktop * android, desktop: update dependency * website(directory): add bot address, open simplex links in new tab, improve error handling * made cover responsive * directory: show only recent groups in active/new tabs, page improvements (#6290) * directory: show only recent groups as active or new * round times * sorting order * fix links * improve * directory: disable search autocomplete * directory: show approximate activity/creation time * scripts: suffix deb packages with distribution codename (#6273) * directory: show default image in case of loading error (can happen when group is delisted) * website: add directory to nav bar, fixes (#6293) * website: add directory to nav bar, fixes * docs: update readme * add responsiveness * add socials * remove white redundent line --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>
This commit is contained in:
co-authored by
Evgeny Poberezkin
sh
parent
46bdaf0a19
commit
c88f99a37b
@@ -54,24 +54,10 @@ If you are interested in helping us to integrate open-source language models, an
|
||||
|
||||
## Join user groups
|
||||
|
||||
You can join the groups created by other users via the new [directory service](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). 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://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.
|
||||
|
||||
**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.
|
||||
|
||||
You also can:
|
||||
- criticize the app, and make comparisons with other messengers.
|
||||
- share new messengers you think could be interesting for privacy, as long as you don't spam.
|
||||
- share some privacy related publications, infrequently.
|
||||
- having preliminary approved with the admin in direct message, share the link to a group you created, but only once. Once the group has more than 10 members it can be submitted to [SimpleX Directory Service](./docs/DIRECTORY.md) where the new users will be able to discover it.
|
||||
|
||||
You must:
|
||||
- be polite to other users
|
||||
- avoid spam (too frequent messages, even if they are relevant)
|
||||
- avoid any personal attacks or hostility.
|
||||
- avoid sharing any content that is not relevant to the above (that includes, but is not limited to, discussing politics or any aspects of society other than privacy, security, technology and communications, sharing any content that may be found offensive by other users, etc.).
|
||||
|
||||
Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment.
|
||||
|
||||
You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://smp4.simplex.im/g#hr4lvFeBmndWMKTwqiodPz3VBo_6UmdGWocXd1SupsM)
|
||||
|
||||
There is also a group [#simplex-devs](https://smp6.simplex.im/g#Drx3efC-n418AuSpzTspw9SER0iJwrQTmKBafQHwkKM) for developers who build on SimpleX platform:
|
||||
@@ -81,11 +67,7 @@ There is also a group [#simplex-devs](https://smp6.simplex.im/g#Drx3efC-n418AuSp
|
||||
- social apps and services
|
||||
- etc.
|
||||
|
||||
There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users:
|
||||
|
||||
[\#SimpleX-DE](https://smp6.simplex.im/g#V6tQ-lJqsdgJJdJiLPtP326oQFKHvwinIbgruZ9K2oU) (German-speaking), [\#SimpleX-ES](https://smp5.simplex.im/g#xJ5kwDLq2305O5FmpUzvgRIXXAcAJ9S5BItCd2Wmloc) (Spanish-speaking), [\#SimpleX-FR](https://smp6.simplex.im/g#cVOpB0CKd6hEf2aWQ6sJ22E2DVgQLtdHoiSdKxXeKqk) (French-speaking), [\#SimpleX-RU](https://smp5.simplex.im/g#vwXRdfG5SgtaG6aVcITiUGd--Ux0rY1IuH4QXYxlq3U) (Russian-speaking), [\#SimpleX-IT](https://smp5.simplex.im/g#BtRcjsl29ULFNBSE2OPhp1UwZfW7PW9gUYFQTKHdjqU) (Italian-speaking).
|
||||
|
||||
You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code.
|
||||
You can join these and other groups by opening these links in the app or by opening them in a desktop browser and scanning the QR code.
|
||||
|
||||
## Follow our updates
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ kotlin {
|
||||
}
|
||||
// For jSystemThemeDetector only
|
||||
implementation("net.java.dev.jna:jna-platform:5.14.0")
|
||||
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
|
||||
implementation("com.sshtools:two-slices:0.9.1")
|
||||
implementation("org.slf4j:slf4j-simple:2.0.12")
|
||||
implementation("uk.co.caprica:vlcj:4.8.3")
|
||||
implementation("net.java.dev.jna:jna:5.14.0")
|
||||
|
||||
@@ -5,8 +5,6 @@ module Main where
|
||||
import Directory.Options
|
||||
import Directory.Service
|
||||
import Directory.Store
|
||||
import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks)
|
||||
import Simplex.Chat.Core
|
||||
import Simplex.Chat.Terminal (terminalChatConfig)
|
||||
|
||||
main :: IO ()
|
||||
@@ -15,11 +13,4 @@ main = do
|
||||
st <- restoreDirectoryStore directoryLog
|
||||
if runCLI
|
||||
then directoryServiceCLI st opts
|
||||
else do
|
||||
env <- newServiceState opts
|
||||
let chatHooks =
|
||||
defaultChatHooks
|
||||
{ postStartHook = Just $ directoryStartHook st opts,
|
||||
acceptMember = Just $ acceptMemberHook opts env
|
||||
}
|
||||
simplexChatCore (terminalChatConfig {chatHooks}) (mkChatOpts opts) $ directoryService st opts env
|
||||
else directoryService st opts terminalChatConfig
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
module Directory.Listing where
|
||||
@@ -11,23 +13,38 @@ module Directory.Listing where
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad
|
||||
import Crypto.Hash (Digest, MD5)
|
||||
import qualified Crypto.Hash as CH
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as JQ
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.ByteString.Base64 as B64
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import qualified Data.ByteString.Base64.URL as B64URL
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy as LB
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Int (Int64)
|
||||
import Data.List (isPrefixOf)
|
||||
import Data.Maybe (catMaybes, fromMaybe)
|
||||
import qualified Data.Set as S
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Time.Clock
|
||||
import Data.Time.Clock.System
|
||||
import Data.Time.Format.ISO8601 (iso8601Show)
|
||||
import Directory.Store
|
||||
import Simplex.Chat.Markdown
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, taggedObjectJSON)
|
||||
import System.Directory
|
||||
import System.FilePath
|
||||
|
||||
directoryDataPath :: String
|
||||
directoryDataPath = "data"
|
||||
|
||||
listingFileName :: String
|
||||
listingFileName = "listing.json"
|
||||
|
||||
@@ -47,9 +64,12 @@ $(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "DET") ''DirectoryEntryType)
|
||||
data DirectoryEntry = DirectoryEntry
|
||||
{ entryType :: DirectoryEntryType,
|
||||
displayName :: Text,
|
||||
shortDescr :: Maybe Text,
|
||||
welcomeMessage :: Maybe Text,
|
||||
imageFile :: Maybe String
|
||||
groupLink :: CreatedLinkContact,
|
||||
shortDescr :: Maybe MarkdownList,
|
||||
welcomeMessage :: Maybe MarkdownList,
|
||||
imageFile :: Maybe String,
|
||||
activeAt :: Maybe UTCTime,
|
||||
createdAt :: Maybe UTCTime
|
||||
}
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''DirectoryEntry)
|
||||
@@ -60,38 +80,77 @@ $(JQ.deriveJSON defaultJSON ''DirectoryListing)
|
||||
|
||||
type ImageFileData = ByteString
|
||||
|
||||
groupDirectoryEntry :: GroupInfoSummary -> (DirectoryEntry, Maybe (FilePath, ImageFileData))
|
||||
groupDirectoryEntry (GIS GroupInfo {groupId, groupProfile} summary) =
|
||||
newOrActive :: NominalDiffTime
|
||||
newOrActive = 30 * nominalDay
|
||||
|
||||
recentRoundedTime :: Int64 -> UTCTime -> UTCTime -> Maybe UTCTime
|
||||
recentRoundedTime roundTo now t
|
||||
| diffUTCTime now t > newOrActive = Nothing
|
||||
| otherwise =
|
||||
let secs = (systemSeconds (utcToSystemTime t) `div` roundTo) * roundTo
|
||||
in Just $ systemToUTCTime $ MkSystemTime secs 0
|
||||
|
||||
groupDirectoryEntry :: UTCTime -> GroupInfoSummary -> Maybe (DirectoryEntry, Maybe (FilePath, ImageFileData))
|
||||
groupDirectoryEntry now (GIS GroupInfo {groupProfile, chatTs, createdAt} summary gLink_) =
|
||||
let GroupProfile {displayName, shortDescr, description, image, memberAdmission} = groupProfile
|
||||
entryType = DETGroup memberAdmission summary
|
||||
imgData = imgFileData =<< image
|
||||
in (DirectoryEntry {entryType, displayName, shortDescr, welcomeMessage = description, imageFile = fst <$> imgData}, imgData)
|
||||
entry groupLink =
|
||||
let de =
|
||||
DirectoryEntry
|
||||
{ entryType,
|
||||
displayName,
|
||||
groupLink,
|
||||
shortDescr = toFormattedText <$> shortDescr,
|
||||
welcomeMessage = toFormattedText <$> description,
|
||||
imageFile = fst <$> imgData,
|
||||
activeAt = recentRoundedTime 900 now $ fromMaybe createdAt chatTs,
|
||||
createdAt = recentRoundedTime 86400 now createdAt
|
||||
}
|
||||
imgData = imgFileData groupLink =<< image
|
||||
in (de, imgData)
|
||||
in (entry . connLinkContact) <$> gLink_
|
||||
where
|
||||
imgFileData (ImageData img) =
|
||||
imgFileData :: CreatedConnLink 'CMContact -> ImageData -> Maybe (FilePath, ByteString)
|
||||
imgFileData groupLink (ImageData img) =
|
||||
let (img', imgExt) =
|
||||
fromMaybe (img, ".jpg") $
|
||||
(,".jpg") <$> T.stripPrefix "data:image/jpg;base64," img
|
||||
<|> (,".png") <$> T.stripPrefix "data:image/png;base64," img
|
||||
imgFile = listingImageFolder </> show groupId <> imgExt
|
||||
imgName = B.unpack $ B64URL.encodeUnpadded $ BA.convert $ (CH.hash :: ByteString -> Digest MD5) $ strEncode (connFullLink groupLink)
|
||||
imgFile = listingImageFolder </> imgName <> imgExt
|
||||
in case B64.decode $ encodeUtf8 img' of
|
||||
Right img'' -> Just (imgFile, img'')
|
||||
Left _ -> Nothing
|
||||
|
||||
generateListing :: DirectoryStore -> FilePath -> [GroupInfoSummary] -> IO ()
|
||||
generateListing st dir gs = do
|
||||
createDirectoryIfMissing True dir
|
||||
oldDirs <- filter ((directoryDataPath <> ".") `isPrefixOf`) <$> listDirectory dir
|
||||
ts <- getCurrentTime
|
||||
let newDirPath = directoryDataPath <> "." <> iso8601Show ts <> "/"
|
||||
newDir = dir </> newDirPath
|
||||
gs' <- filterListedGroups st gs
|
||||
removePathForcibly (dir </> listingImageFolder)
|
||||
createDirectoryIfMissing True (dir </> listingImageFolder)
|
||||
gs'' <- forM gs' $ \g@(GIS GroupInfo {groupId} _) -> do
|
||||
let (g', img) = groupDirectoryEntry g
|
||||
forM_ img $ \(imgFile, imgData) -> B.writeFile (dir </> imgFile) imgData
|
||||
pure (groupId, g')
|
||||
saveListing listingFileName gs''
|
||||
saveListing promotedFileName =<< filterPromotedGroups st gs''
|
||||
createDirectoryIfMissing True (newDir </> listingImageFolder)
|
||||
gs'' <-
|
||||
fmap catMaybes $ forM gs' $ \g@(GIS GroupInfo {groupId} _ _) ->
|
||||
forM (groupDirectoryEntry ts g) $ \(g', img) -> do
|
||||
forM_ img $ \(imgFile, imgData) -> B.writeFile (newDir </> imgFile) imgData
|
||||
pure (groupId, g')
|
||||
saveListing newDir listingFileName gs''
|
||||
saveListing newDir promotedFileName =<< filterPromotedGroups st gs''
|
||||
-- atomically update the link
|
||||
let newSymLink = newDir <> ".link"
|
||||
symLink = dir </> directoryDataPath
|
||||
createDirectoryLink newDirPath newSymLink
|
||||
renamePath newSymLink symLink
|
||||
mapM_ (removePathForcibly . (dir </>)) oldDirs
|
||||
where
|
||||
saveListing f = LB.writeFile (dir </> f) . J.encode . DirectoryListing . map snd
|
||||
saveListing newDir f = LB.writeFile (newDir </> f) . J.encode . DirectoryListing . map snd
|
||||
|
||||
filterPromotedGroups :: DirectoryStore -> [(GroupId, DirectoryEntry)] -> IO [(GroupId, DirectoryEntry)]
|
||||
filterPromotedGroups st gs = do
|
||||
pgs <- readTVarIO $ promotedGroups st
|
||||
pure $ filter (\g -> fst g `S.member` pgs) gs
|
||||
|
||||
toFormattedText :: Text -> MarkdownList
|
||||
toFormattedText t = fromMaybe [FormattedText Nothing t] $ parseMaybeMarkdownList t
|
||||
|
||||
@@ -20,13 +20,13 @@ data SearchRequest = SearchRequest
|
||||
data SearchType = STAll | STRecent | STSearch Text
|
||||
|
||||
takeTop :: Int -> [GroupInfoSummary] -> [GroupInfoSummary]
|
||||
takeTop n = take n . sortOn (\(GIS _ GroupSummary {currentMembers}) -> Down currentMembers)
|
||||
takeTop n = take n . sortOn (\(GIS _ GroupSummary {currentMembers} _) -> Down currentMembers)
|
||||
|
||||
takeRecent :: Int -> [GroupInfoSummary] -> [GroupInfoSummary]
|
||||
takeRecent n = take n . sortOn (\(GIS GroupInfo {createdAt} _) -> Down createdAt)
|
||||
takeRecent n = take n . sortOn (\(GIS GroupInfo {createdAt} _ _) -> Down createdAt)
|
||||
|
||||
groupIds :: [GroupInfoSummary] -> Set GroupId
|
||||
groupIds = S.fromList . map (\(GIS GroupInfo {groupId} _) -> groupId)
|
||||
groupIds = S.fromList . map (\(GIS GroupInfo {groupId} _ _) -> groupId)
|
||||
|
||||
filterNotSent :: Set GroupId -> [GroupInfoSummary] -> [GroupInfoSummary]
|
||||
filterNotSent sentGroups = filter (\(GIS GroupInfo {groupId} _) -> groupId `S.notMember` sentGroups)
|
||||
filterNotSent sentGroups = filter (\(GIS GroupInfo {groupId} _ _) -> groupId `S.notMember` sentGroups)
|
||||
|
||||
@@ -21,7 +21,6 @@ module Directory.Service
|
||||
where
|
||||
|
||||
import Control.Concurrent (forkIO)
|
||||
import Control.Concurrent.Async
|
||||
import Control.Concurrent.STM
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
@@ -54,7 +53,6 @@ import Simplex.Chat.Markdown (Format (..), FormattedText (..), parseMaybeMarkdow
|
||||
import Simplex.Chat.Messages
|
||||
import Simplex.Chat.Options
|
||||
import Simplex.Chat.Protocol (MsgContent (..))
|
||||
import Simplex.Chat.Store (GroupLink (..))
|
||||
import Simplex.Chat.Store.Direct (getContact)
|
||||
import Simplex.Chat.Store.Groups (getGroupInfo, getGroupLink, getGroupSummary, getUserGroupsWithSummary, setGroupCustomData)
|
||||
import Simplex.Chat.Store.Profiles (GroupLinkInfo (..), getGroupLinkInfo)
|
||||
@@ -69,7 +67,7 @@ import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnectionLink (.
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (safeDecodeUtf8, tshow, unlessM, whenM, ($>>=), (<$$>))
|
||||
import Simplex.Messaging.Util (raceAny_, safeDecodeUtf8, tshow, unlessM, whenM, ($>>=), (<$$>))
|
||||
import System.Directory (getAppUserDataDirectory)
|
||||
import System.Exit (exitFailure)
|
||||
import System.Process (readProcess)
|
||||
@@ -96,7 +94,8 @@ data GroupRolesStatus
|
||||
data ServiceState = ServiceState
|
||||
{ searchRequests :: TMap ContactId SearchRequest,
|
||||
blockedWordsCfg :: BlockedWordsConfig,
|
||||
pendingCaptchas :: TMap GroupMemberId PendingCaptcha
|
||||
pendingCaptchas :: TMap GroupMemberId PendingCaptcha,
|
||||
updateListingsJob :: TMVar ChatController
|
||||
}
|
||||
|
||||
data PendingCaptcha = PendingCaptcha
|
||||
@@ -119,7 +118,8 @@ newServiceState opts = do
|
||||
searchRequests <- TM.emptyIO
|
||||
blockedWordsCfg <- readBlockedWordsConfig opts
|
||||
pendingCaptchas <- TM.emptyIO
|
||||
pure ServiceState {searchRequests, blockedWordsCfg, pendingCaptchas}
|
||||
updateListingsJob <- newEmptyTMVarIO
|
||||
pure ServiceState {searchRequests, blockedWordsCfg, pendingCaptchas, updateListingsJob}
|
||||
|
||||
welcomeGetOpts :: IO DirectoryOpts
|
||||
welcomeGetOpts = do
|
||||
@@ -146,22 +146,41 @@ directoryServiceCLI st opts = do
|
||||
env <- newServiceState opts
|
||||
eventQ <- newTQueueIO
|
||||
let eventHook cc resp = atomically $ resp <$ writeTQueue eventQ (cc, resp)
|
||||
chatHooks = defaultChatHooks {postStartHook = Just $ directoryStartHook st opts, eventHook = Just eventHook, acceptMember = Just $ acceptMemberHook opts env}
|
||||
race_
|
||||
(simplexChatCLI' terminalChatConfig {chatHooks} (mkChatOpts opts) Nothing)
|
||||
(processEvents eventQ env)
|
||||
chatHooks = defaultChatHooks {postStartHook = Just $ directoryStartHook opts env, eventHook = Just eventHook, acceptMember = Just $ acceptMemberHook opts env}
|
||||
raceAny_ $
|
||||
[ simplexChatCLI' terminalChatConfig {chatHooks} (mkChatOpts opts) Nothing,
|
||||
processEvents eventQ env
|
||||
]
|
||||
<> updateListingsThread_ st opts env
|
||||
where
|
||||
processEvents eventQ env = forever $ do
|
||||
(cc, resp) <- atomically $ readTQueue eventQ
|
||||
u_ <- readTVarIO (currentUser cc)
|
||||
forM_ u_ $ \user -> directoryServiceEvent st opts env user cc resp
|
||||
|
||||
directoryStartHook :: DirectoryStore -> DirectoryOpts -> ChatController -> IO ()
|
||||
directoryStartHook st opts cc =
|
||||
updateListingDelay :: Int
|
||||
updateListingDelay = 15 * 60 * 1000000 -- update every 15 minutes
|
||||
|
||||
updateListingsThread_ :: DirectoryStore -> DirectoryOpts -> ServiceState -> [IO ()]
|
||||
updateListingsThread_ st opts env = maybe [] (\f -> [updateListingsThread f]) $ webFolder opts
|
||||
where
|
||||
updateListingsThread f = do
|
||||
cc <- atomically $ takeTMVar $ updateListingsJob env
|
||||
forever $ do
|
||||
u <- readTVarIO $ currentUser cc
|
||||
forM_ u $ \user -> updateGroupListingFiles cc st user f
|
||||
delay <- registerDelay updateListingDelay
|
||||
atomically $ void (takeTMVar $ updateListingsJob env) `orElse` unlessM (readTVar delay) retry
|
||||
|
||||
listingsUpdated :: ServiceState -> ChatController -> IO ()
|
||||
listingsUpdated env = void . atomically . tryPutTMVar (updateListingsJob env)
|
||||
|
||||
directoryStartHook :: DirectoryOpts -> ServiceState -> ChatController -> IO ()
|
||||
directoryStartHook opts env cc =
|
||||
readTVarIO (currentUser cc) >>= \case
|
||||
Nothing -> putStrLn "No current user" >> exitFailure
|
||||
Just user@User {userId, profile = p@LocalProfile {preferences}} -> do
|
||||
forM_ (webFolder opts) $ updateGroupListingFiles cc st user
|
||||
Just User {userId, profile = p@LocalProfile {preferences}} -> do
|
||||
listingsUpdated env cc
|
||||
let cmds = fromMaybe [] $ preferences >>= commands_
|
||||
unless (cmds == directoryCommands) $ do
|
||||
let prefs = (fromMaybe emptyChatPrefs preferences) {files = Just FilesPreference {allow = FANo}, commands = Just directoryCommands} :: Preferences
|
||||
@@ -188,12 +207,23 @@ directoryCommands =
|
||||
where
|
||||
idParam = Just "<ID>"
|
||||
|
||||
directoryService :: DirectoryStore -> DirectoryOpts -> ServiceState -> User -> ChatController -> IO ()
|
||||
directoryService st opts@DirectoryOpts {testing} env user cc = do
|
||||
initializeBotAddress' (not testing) cc
|
||||
race_ (forever $ void getLine) . forever $ do
|
||||
(_, resp) <- atomically . readTBQueue $ outputQ cc
|
||||
directoryServiceEvent st opts env user cc resp
|
||||
directoryService :: DirectoryStore -> DirectoryOpts -> ChatConfig -> IO ()
|
||||
directoryService st opts@DirectoryOpts {testing} cfg = do
|
||||
env <- newServiceState opts
|
||||
let chatHooks =
|
||||
defaultChatHooks
|
||||
{ postStartHook = Just $ directoryStartHook opts env,
|
||||
acceptMember = Just $ acceptMemberHook opts env
|
||||
}
|
||||
simplexChatCore cfg {chatHooks} (mkChatOpts opts) $ \user cc -> do
|
||||
initializeBotAddress' (not testing) cc
|
||||
raceAny_ $
|
||||
[ forever $ void getLine,
|
||||
forever $ do
|
||||
(_, resp) <- atomically . readTBQueue $ outputQ cc
|
||||
directoryServiceEvent st opts env user cc resp
|
||||
]
|
||||
<> updateListingsThread_ st opts env
|
||||
|
||||
acceptMemberHook :: DirectoryOpts -> ServiceState -> GroupInfo -> GroupLinkInfo -> Profile -> IO (Either GroupRejectionReason (GroupAcceptance, GroupMemberRole))
|
||||
acceptMemberHook
|
||||
@@ -301,7 +331,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
getDuplicateGroup GroupInfo {groupId, groupProfile = GroupProfile {displayName, fullName}} =
|
||||
getGroups fullName >>= mapM duplicateGroup
|
||||
where
|
||||
sameGroupNotRemoved (GIS g@GroupInfo {groupId = gId, groupProfile = GroupProfile {displayName = n, fullName = fn}} _) =
|
||||
sameGroupNotRemoved (GIS g@GroupInfo {groupId = gId, groupProfile = GroupProfile {displayName = n, fullName = fn}} _ _) =
|
||||
gId /= groupId && n == displayName && fn == fullName && not (memberRemoved $ membership g)
|
||||
duplicateGroup [] = pure DGUnique
|
||||
duplicateGroup groups = do
|
||||
@@ -310,13 +340,13 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
then pure DGUnique
|
||||
else do
|
||||
(lgs, rgs) <- atomically $ (,) <$> readTVar (listedGroups st) <*> readTVar (reservedGroups st)
|
||||
let reserved = any (\(GIS GroupInfo {groupId = gId} _) -> gId `S.member` lgs || gId `S.member` rgs) gs
|
||||
let reserved = any (\(GIS GroupInfo {groupId = gId} _ _) -> gId `S.member` lgs || gId `S.member` rgs) gs
|
||||
if reserved
|
||||
then pure DGReserved
|
||||
else do
|
||||
removed <- foldM (\r -> fmap (r &&) . isGroupRemoved) True gs
|
||||
pure $ if removed then DGUnique else DGRegistered
|
||||
isGroupRemoved (GIS GroupInfo {groupId = gId} _) =
|
||||
isGroupRemoved (GIS GroupInfo {groupId = gId} _ _) =
|
||||
getGroupReg st gId >>= \case
|
||||
Just GroupReg {groupRegStatus} -> groupRemoved <$> readTVarIO groupRegStatus
|
||||
Nothing -> pure True
|
||||
@@ -395,7 +425,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
notifyOwner gr $ "Joined the group " <> displayName <> ", creating the link…"
|
||||
sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case
|
||||
Right CRGroupLinkCreated {groupLink = GroupLink {connLinkContact = gLink}} -> do
|
||||
setGroupStatus st opts cc user gr GRSPendingUpdate
|
||||
setGroupStatus st env cc gr GRSPendingUpdate
|
||||
notifyOwner
|
||||
gr
|
||||
"Created the public link to join the group via this directory service that is always online.\n\n\
|
||||
@@ -456,7 +486,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
Just DGReserved -> notifyOwner gr $ groupAlreadyListed toGroup
|
||||
_ -> do
|
||||
let gaId = 1
|
||||
setGroupStatus st opts cc user gr $ GRSPendingApproval gaId
|
||||
setGroupStatus st env cc gr $ GRSPendingApproval gaId
|
||||
notifyOwner gr $
|
||||
("Thank you! The group link for " <> userGroupReference gr toGroup <> " is added to the welcome message" <> byMember)
|
||||
<> ".\nYou will be notified once the group is added to the directory - it may take up to 48 hours."
|
||||
@@ -466,18 +496,18 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
groupRef = groupReference toGroup
|
||||
groupProfileUpdate >>= \case
|
||||
GPNoServiceLink -> do
|
||||
setGroupStatus st opts cc user gr GRSPendingUpdate
|
||||
setGroupStatus st env cc gr GRSPendingUpdate
|
||||
notifyOwner gr $
|
||||
("The group profile is updated for " <> userGroupRef <> byMember <> ", but no link is added to the welcome message.\n\n")
|
||||
<> "The group will remain hidden from the directory until the group link is added and the group is re-approved."
|
||||
GPServiceLinkRemoved -> do
|
||||
setGroupStatus st opts cc user gr GRSPendingUpdate
|
||||
setGroupStatus st env cc gr GRSPendingUpdate
|
||||
notifyOwner gr $
|
||||
("The group link for " <> userGroupRef <> " is removed from the welcome message" <> byMember)
|
||||
<> ".\n\nThe group is hidden from the directory until the group link is added and the group is re-approved."
|
||||
notifyAdminUsers $ "The group link is removed from " <> groupRef <> ", de-listed."
|
||||
GPServiceLinkAdded _ -> do
|
||||
setGroupStatus st opts cc user gr $ GRSPendingApproval n'
|
||||
setGroupStatus st env cc gr $ GRSPendingApproval n'
|
||||
notifyOwner gr $
|
||||
("The group link is added to " <> userGroupRef <> byMember)
|
||||
<> "!\nIt is hidden from the directory until approved."
|
||||
@@ -490,7 +520,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
<> "!\nThe group is listed in directory."
|
||||
notifyAdminUsers $ "The group " <> groupRef <> " is updated" <> byMember <> " - only link or whitespace changes.\nThe group remained listed in directory."
|
||||
| otherwise -> do
|
||||
setGroupStatus st opts cc user gr $ GRSPendingApproval n'
|
||||
setGroupStatus st env cc gr $ GRSPendingApproval n'
|
||||
notifyOwner gr $
|
||||
("The group " <> userGroupRef <> " is updated" <> byMember)
|
||||
<> "!\nIt is hidden from the directory until approved."
|
||||
@@ -628,14 +658,14 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
when (ctId `isOwner` gr) $ do
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSSuspendedBadRoles -> when (rStatus == GRSOk) $ do
|
||||
setGroupStatus st opts cc user gr GRSActive
|
||||
setGroupStatus st env cc gr GRSActive
|
||||
notifyOwner gr $ uCtRole <> ".\n\nThe group is listed in the directory again."
|
||||
notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suCtRole
|
||||
GRSPendingApproval gaId -> when (rStatus == GRSOk) $ do
|
||||
sendToApprove g gr gaId
|
||||
notifyOwner gr $ uCtRole <> ".\n\nThe group is submitted for approval."
|
||||
GRSActive -> when (rStatus /= GRSOk) $ do
|
||||
setGroupStatus st opts cc user gr GRSSuspendedBadRoles
|
||||
setGroupStatus st env cc gr GRSSuspendedBadRoles
|
||||
notifyOwner gr $ uCtRole <> ".\n\nThe group is no longer listed in the directory."
|
||||
notifyAdminUsers $ "The group " <> groupRef <> " is de-listed " <> suCtRole
|
||||
_ -> pure ()
|
||||
@@ -654,7 +684,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSSuspendedBadRoles -> when (serviceRole == GRAdmin) $
|
||||
whenContactIsOwner gr $ do
|
||||
setGroupStatus st opts cc user gr GRSActive
|
||||
setGroupStatus st env cc gr GRSActive
|
||||
notifyOwner gr $ uSrvRole <> ".\n\nThe group is listed in the directory again."
|
||||
notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suSrvRole
|
||||
GRSPendingApproval gaId -> when (serviceRole == GRAdmin) $
|
||||
@@ -662,7 +692,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
sendToApprove g gr gaId
|
||||
notifyOwner gr $ uSrvRole <> ".\n\nThe group is submitted for approval."
|
||||
GRSActive -> when (serviceRole /= GRAdmin) $ do
|
||||
setGroupStatus st opts cc user gr GRSSuspendedBadRoles
|
||||
setGroupStatus st env cc gr GRSSuspendedBadRoles
|
||||
notifyOwner gr $ uSrvRole <> ".\n\nThe group is no longer listed in the directory."
|
||||
notifyAdminUsers $ "The group " <> groupRef <> " is de-listed " <> suSrvRole
|
||||
_ -> pure ()
|
||||
@@ -679,7 +709,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
logInfo $ "contact ID " <> tshow ctId <> " removed from group " <> viewGroupName g
|
||||
withGroupReg g "contact removed" $ \gr -> do
|
||||
when (ctId `isOwner` gr) $ do
|
||||
setGroupStatus st opts cc user gr GRSRemoved
|
||||
setGroupStatus st env cc gr GRSRemoved
|
||||
notifyOwner gr $ "You are removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory."
|
||||
notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)."
|
||||
|
||||
@@ -688,7 +718,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
logInfo $ "contact ID " <> tshow ctId <> " left group " <> viewGroupName g
|
||||
withGroupReg g "contact left" $ \gr -> do
|
||||
when (ctId `isOwner` gr) $ do
|
||||
setGroupStatus st opts cc user gr GRSRemoved
|
||||
setGroupStatus st env cc gr GRSRemoved
|
||||
notifyOwner gr $ "You left the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory."
|
||||
notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)."
|
||||
|
||||
@@ -696,7 +726,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
deServiceRemovedFromGroup g = do
|
||||
logInfo $ "service removed from group " <> viewGroupName g
|
||||
withGroupReg g "service removed" $ \gr -> do
|
||||
setGroupStatus st opts cc user gr GRSRemoved
|
||||
setGroupStatus st env cc gr GRSRemoved
|
||||
notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory."
|
||||
notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)."
|
||||
|
||||
@@ -704,7 +734,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
deGroupDeleted g = do
|
||||
logInfo $ "group removed " <> viewGroupName g
|
||||
withGroupReg g "group removed" $ \gr -> do
|
||||
setGroupStatus st opts cc user gr GRSRemoved
|
||||
setGroupStatus st env cc gr GRSRemoved
|
||||
notifyOwner gr $ "The group " <> userGroupReference gr g <> " is deleted.\n\nThe group is no longer listed in the directory."
|
||||
notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group is deleted)."
|
||||
|
||||
@@ -925,7 +955,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
where
|
||||
msgs = replyMsg :| map foundGroup gs <> [moreMsg | moreGroups > 0]
|
||||
replyMsg = (Just ciId, MCText reply)
|
||||
foundGroup (GIS GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_}} GroupSummary {currentMembers}) =
|
||||
foundGroup (GIS GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_}} GroupSummary {currentMembers} _) =
|
||||
let membersStr = "_" <> tshow currentMembers <> " members_"
|
||||
showId = if isAdmin then tshow groupId <> ". " else ""
|
||||
text = showId <> groupInfoText p <> "\n" <> membersStr
|
||||
@@ -946,16 +976,16 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
_ -> do
|
||||
getGroupRolesStatus g gr >>= \case
|
||||
Just GRSOk -> do
|
||||
setGroupStatus st opts cc user gr GRSActive
|
||||
setGroupStatus st env cc gr GRSActive
|
||||
forM_ promote $ \promo ->
|
||||
if promo -- admins can unpromote, only super-user can promote when approving
|
||||
then
|
||||
unlessM (readTVarIO promoted) $
|
||||
if knownCt `elem` superUsers
|
||||
then setGroupPromoted st opts cc user gr True
|
||||
then setGroupPromoted st env cc gr True
|
||||
else sendReply "You cannot promote groups"
|
||||
else do
|
||||
whenM (readTVarIO promoted) $ setGroupPromoted st opts cc user gr False
|
||||
whenM (readTVarIO promoted) $ setGroupPromoted st env cc gr False
|
||||
notifyOtherSuperUsers $ "Group promotion is disabled for " <> groupRef
|
||||
let approved = "The group " <> userGroupReference' gr n <> " is approved"
|
||||
notifyOwner gr $
|
||||
@@ -991,7 +1021,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
withGroupAndReg sendReply groupId gName $ \_ gr ->
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSActive -> do
|
||||
setGroupStatus st opts cc user gr GRSSuspended
|
||||
setGroupStatus st env cc gr GRSSuspended
|
||||
let suspended = "The group " <> userGroupReference' gr gName <> " is suspended"
|
||||
notifyOwner gr $ suspended <> " and hidden from directory. Please contact the administrators."
|
||||
sendReply "Group suspended!"
|
||||
@@ -1002,7 +1032,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
withGroupAndReg sendReply groupId gName $ \_ gr ->
|
||||
readTVarIO (groupRegStatus gr) >>= \case
|
||||
GRSSuspended -> do
|
||||
setGroupStatus st opts cc user gr GRSActive
|
||||
setGroupStatus st env cc gr GRSActive
|
||||
let groupStr = "The group " <> userGroupReference' gr gName
|
||||
notifyOwner gr $ groupStr <> " is listed in the directory again!"
|
||||
sendReply "Group listing resumed!"
|
||||
@@ -1078,7 +1108,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
withGroupAndReg sendReply groupId gName $ \_ gr@GroupReg {groupRegStatus, promoted} -> do
|
||||
status <- readTVarIO groupRegStatus
|
||||
promote <- readTVarIO promoted
|
||||
when (promote' /= promote) $ setGroupPromoted st opts cc user gr promote'
|
||||
when (promote' /= promote) $ setGroupPromoted st env cc gr promote'
|
||||
let msg =
|
||||
"Group promotion "
|
||||
<> (if promote' then "enabled" <> (if status == GRSActive then "." else ", but the group is not listed.") else "disabled.")
|
||||
@@ -1132,18 +1162,16 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
let text = T.unlines $ [tshow useGroupId <> ". Error: getGroup. Please notify the developers."] <> maybeToList ownerStr_ <> [statusStr]
|
||||
sendComposedMessage cc ct Nothing $ MCText text
|
||||
|
||||
setGroupStatus :: DirectoryStore -> DirectoryOpts -> ChatController -> User -> GroupReg -> GroupRegStatus -> IO ()
|
||||
setGroupStatus st opts cc u gr grStatus' = do
|
||||
setGroupStatus :: DirectoryStore -> ServiceState -> ChatController -> GroupReg -> GroupRegStatus -> IO ()
|
||||
setGroupStatus st env cc gr grStatus' = do
|
||||
let status' = grDirectoryStatus grStatus'
|
||||
status <- setGroupStatusStore st gr grStatus'
|
||||
forM_ (webFolder opts) $ \dir ->
|
||||
when ((status == DSListed || status' == DSListed) && status /= status') $ updateGroupListingFiles cc st u dir
|
||||
when ((status == DSListed || status' == DSListed) && status /= status') $ listingsUpdated env cc
|
||||
|
||||
setGroupPromoted :: DirectoryStore -> DirectoryOpts -> ChatController -> User -> GroupReg -> Bool -> IO ()
|
||||
setGroupPromoted st opts cc u gr grPromoted' = do
|
||||
setGroupPromoted :: DirectoryStore -> ServiceState -> ChatController -> GroupReg -> Bool -> IO ()
|
||||
setGroupPromoted st env cc gr grPromoted' = do
|
||||
(status, grPromoted) <- setGroupPromotedStore st gr grPromoted'
|
||||
forM_ (webFolder opts) $ \dir ->
|
||||
when (status == DSListed && grPromoted' /= grPromoted) $ updateGroupListingFiles cc st u dir
|
||||
when (status == DSListed && grPromoted' /= grPromoted) $ listingsUpdated env cc
|
||||
|
||||
updateGroupListingFiles :: ChatController -> DirectoryStore -> User -> FilePath -> IO ()
|
||||
updateGroupListingFiles cc st u dir =
|
||||
|
||||
@@ -273,7 +273,7 @@ getUserGroupRegs st ctId = filter ((ctId ==) . dbContactId) <$> readTVarIO (grou
|
||||
filterListedGroups :: DirectoryStore -> [GroupInfoSummary] -> IO [GroupInfoSummary]
|
||||
filterListedGroups st gs = do
|
||||
lgs <- readTVarIO $ listedGroups st
|
||||
pure $ filter (\(GIS GroupInfo {groupId} _) -> groupId `S.member` lgs) gs
|
||||
pure $ filter (\(GIS GroupInfo {groupId} _ _) -> groupId `S.member` lgs) gs
|
||||
|
||||
listGroup :: DirectoryStore -> GroupReg -> STM ()
|
||||
listGroup st gr = do
|
||||
|
||||
@@ -2159,6 +2159,7 @@ MemberSupport:
|
||||
**Record type**:
|
||||
- groupInfo: [GroupInfo](#groupinfo)
|
||||
- groupSummary: [GroupSummary](#groupsummary)
|
||||
- groupLink: [GroupLink](#grouplink)?
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -29,7 +29,6 @@ import Simplex.Chat.Messages
|
||||
import Simplex.Chat.Messages.CIContent
|
||||
import Simplex.Chat.Messages.CIContent.Events
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Store.Groups
|
||||
import Simplex.Chat.Store.Profiles
|
||||
import Simplex.Chat.Store.Shared
|
||||
import Simplex.Chat.Types
|
||||
@@ -69,7 +68,7 @@ chatTypesDocs = sortOn docTypeName $! snd $! mapAccumL toCTDoc (S.empty, M.empty
|
||||
let (tds', td_) = toTypeDef tds sumTypeInfo
|
||||
in case td_ of
|
||||
Just typeDef -> (tds', CTDoc {typeDef, typeSyntax, typeDescr})
|
||||
Nothing -> error $ "Recursive type: " <> typeName
|
||||
Nothing -> error $ "Recursive type: " <> typeName
|
||||
|
||||
toTypeDef :: (S.Set String, M.Map String APITypeDef) -> (SumTypeInfo, SumTypeJsonEncoding, String, [ConsName], Expr, Text) -> ((S.Set String, M.Map String APITypeDef), Maybe APITypeDef)
|
||||
toTypeDef acc@(!visited, !typeDefs) (STI typeName allConstrs, jsonEncoding, consPrefix, hideConstrs, _, _) =
|
||||
@@ -84,7 +83,7 @@ toTypeDef acc@(!visited, !typeDefs) (STI typeName allConstrs, jsonEncoding, cons
|
||||
let fields = fromMaybe (error $ "Record type without fields: " <> typeName) $ L.nonEmpty fieldInfos
|
||||
((visited', typeDefs'), fields') = mapAccumL (toAPIField_ typeName) (S.insert typeName visited, typeDefs) fields
|
||||
td = APITypeDef typeName $ ATDRecord $ L.toList fields'
|
||||
in ((S.insert typeName visited', M.insert typeName td typeDefs'), Just td)
|
||||
in ((S.insert typeName visited', M.insert typeName td typeDefs'), Just td)
|
||||
_ -> error $ "Record type with " <> show (length constrs) <> " constructors: " <> typeName
|
||||
STUnion -> if length constrs > 1 then toUnionType constrs else unionError constrs
|
||||
STUnion1 -> if length constrs == 1 then toUnionType constrs else unionError constrs
|
||||
@@ -98,16 +97,16 @@ toTypeDef acc@(!visited, !typeDefs) (STI typeName allConstrs, jsonEncoding, cons
|
||||
toUnionType constrs =
|
||||
let ((visited', typeDefs'), members) = mapAccumL toUnionMember (S.insert typeName visited, typeDefs) $ fromMaybe (unionError constrs) $ L.nonEmpty constrs
|
||||
td = APITypeDef typeName $ ATDUnion members
|
||||
in ((S.insert typeName visited', M.insert typeName td typeDefs'), Just td)
|
||||
in ((S.insert typeName visited', M.insert typeName td typeDefs'), Just td)
|
||||
toUnionMember tds RecordTypeInfo {consName, fieldInfos} =
|
||||
let memberTag = normalizeConsName consPrefix consName
|
||||
in second (ATUnionMember memberTag) $ mapAccumL (toAPIField_ typeName) tds fieldInfos
|
||||
in second (ATUnionMember memberTag) $ mapAccumL (toAPIField_ typeName) tds fieldInfos
|
||||
unionError constrs = error $ "Union type with " <> show (length constrs) <> " constructor(s): " <> typeName
|
||||
toEnumType = toEnumType_ $ normalizeConsName consPrefix
|
||||
toEnumType_ f constrs =
|
||||
let members = L.map toEnumMember $ fromMaybe (enumError constrs) $ L.nonEmpty constrs
|
||||
td = APITypeDef typeName $ ATDEnum members
|
||||
in ((S.insert typeName visited, M.insert typeName td typeDefs), Just td)
|
||||
in ((S.insert typeName visited, M.insert typeName td typeDefs), Just td)
|
||||
where
|
||||
toEnumMember RecordTypeInfo {consName, fieldInfos} = case fieldInfos of
|
||||
[] -> f consName
|
||||
@@ -121,7 +120,7 @@ toAPIField_ typeName tds (FieldInfo fieldName typeInfo) = second (APIRecordField
|
||||
toAPIType = \case
|
||||
TIType (ST name _) -> apiTypeForName name
|
||||
TIOptional tInfo -> second ATOptional $ toAPIType tInfo
|
||||
TIArray {elemType, nonEmpty} -> second (`ATArray`nonEmpty) $ toAPIType elemType
|
||||
TIArray {elemType, nonEmpty} -> second (`ATArray` nonEmpty) $ toAPIType elemType
|
||||
TIMap {keyType = ST name _, valueType}
|
||||
| name `elem` primitiveTypes -> second (ATMap (PT name)) $ toAPIType valueType
|
||||
| otherwise -> error $ "Non-primitive key type in " <> typeName <> ", " <> fieldName
|
||||
@@ -133,7 +132,7 @@ toAPIField_ typeName tds (FieldInfo fieldName typeInfo) = second (APIRecordField
|
||||
Nothing -> case find (\(STI name' _, _, _, _, _, _) -> name == name') chatTypesDocsData of
|
||||
Just sumTypeInfo ->
|
||||
let (tds', td_) = toTypeDef tds sumTypeInfo -- recursion to outer function, loops are resolved via type defs map lookup
|
||||
in case td_ of
|
||||
in case td_ of
|
||||
Just td -> (tds', ATDef td)
|
||||
Nothing -> (tds', ATRef name)
|
||||
Nothing -> error $ "Undefined type: " <> name
|
||||
@@ -352,7 +351,6 @@ chatTypesDocsData =
|
||||
(sti @XFTPErrorType, STUnion, "", [], "", ""),
|
||||
(sti @XFTPRcvFile, STRecord, "", [], "", ""),
|
||||
(sti @XFTPSndFile, STRecord, "", [], "", "")
|
||||
|
||||
-- (sti @DatabaseError, STUnion, "DB", [], "", ""),
|
||||
-- (sti @ChatItemInfo, STRecord, "", [], "", ""),
|
||||
-- (sti @ChatItemVersion, STRecord, "", [], "", ""),
|
||||
@@ -371,7 +369,7 @@ chatTypesDocsData =
|
||||
-- (sti @SendRef, STRecord, "", [], "", ""),
|
||||
-- (sti @SndQueueInfo, STRecord, "", [], "", ""),
|
||||
-- (sti @SndSwitchStatus, STEnum, "", [], "", ""), -- incorrect
|
||||
]
|
||||
]
|
||||
|
||||
data SimplePreference = SimplePreference {allow :: FeatureAllowed} deriving (Generic)
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
layout: layouts/jobs.html
|
||||
permalink: /about/index.html
|
||||
---
|
||||
|
||||
# About us
|
||||
|
||||
SimpleX Chat Ltd is a company founded to develop SimpleX network and software.
|
||||
|
||||
Our mission is to create a fully decentralized network, based on the same principles as open web, but the one that gives users full control and ownership of their identity, contacts and communities.
|
||||
|
||||
## Contact us
|
||||
|
||||
SimpleX: "Ask SimpleX team" contact in the app or [this address](https://simplex.chat/contact#/?v=2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
|
||||
|
||||
Email: [chat@simplex.chat](mailto:chat@simplex.chat). You can use PGP to encrypt email messages using our key from [keys.openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat) (its fingerprint is `FB44 AF81 A45B DE32 7319 797C 8510 7E35 7D4A 17FC`) and making your key available for a secure reply.
|
||||
|
||||
You can follow our updates on social media: [X/Twitter](https://x.com/simplexchat), [Reddit](https://www.reddit.com/r/SimpleXChat/), [Mastodon](https://mastodon.social/@simplex) and [Nostr](https://primal.net/p/npub1exv22uulqnmlluszc4yk92jhs2e5ajcs6mu3t00a6avzjcalj9csm7d828).
|
||||
+6
-4
@@ -1,15 +1,17 @@
|
||||
---
|
||||
title: SimpleX Directory Service
|
||||
title: SimpleX Directory
|
||||
revision: 18.08.2023
|
||||
---
|
||||
|
||||
# SimpleX Directory Service
|
||||
# SimpleX Directory
|
||||
|
||||
You can use an experimental directory service to discover the groups created and registered by other users.
|
||||
You can use SimpleX Directory to discover the groups created and registered by other users.
|
||||
|
||||
## Searching for groups
|
||||
|
||||
Connect to the directory service via [this address](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) and send the message containing the words you want to find in the group name or welcome message. You will receive up to 10 groups with the largest number of members in the response, together with the links to join these groups.
|
||||
SimpleX Directory is available at [simplex.chat/directory](https://simplex.chat/directory/) or via this [onion link](http://isdb4l77sjqoy2qq7ipum6x3at6hyn3jmxfx4zdhc72ufbmuq4ilwkqd.onion/directory/).
|
||||
|
||||
You can also connect to SimpleX Directory via [this address](https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok) and send the message containing the words you want to find in the group name or welcome message. You will receive up to 10 groups with the largest number of members in the response, together with the links to join these groups.
|
||||
|
||||
Please note that your search queries can be kept by the bot as the conversation history, but you can use incognito mode when connecting to the bot, to avoid correlation with any other communications. See [Privacy policy](../PRIVACY.md) for more details.
|
||||
|
||||
|
||||
@@ -2451,6 +2451,7 @@ export interface GroupInfo {
|
||||
export interface GroupInfoSummary {
|
||||
groupInfo: GroupInfo
|
||||
groupSummary: GroupSummary
|
||||
groupLink?: GroupLink
|
||||
}
|
||||
|
||||
export interface GroupLink {
|
||||
|
||||
@@ -37,8 +37,12 @@ export SOURCE_DATE_EPOCH=1704067200
|
||||
|
||||
dpkg-deb -R ./release/main/deb/simplex*.deb ./extracted
|
||||
|
||||
# Source the distribution variables (VERSION_CODENAME)
|
||||
. /etc/os-release
|
||||
|
||||
rm -f ./extracted/opt/*imple*/lib/app/*skiko-awt-runtime-linux*
|
||||
sed -i -e '/skiko-awt-runtime-linux/d' ./extracted/opt/*imple*/lib/app/simplex.cfg
|
||||
sed -i "/Version/ s/\$/~$VERSION_CODENAME/" ./extracted/DEBIAN/control
|
||||
find ./extracted/ -exec touch -d "@$SOURCE_DATE_EPOCH" {} +
|
||||
|
||||
dpkg-deb --build --root-owner-group --uniform-compression ./extracted ./release/main/deb/simplex_${ARCH}.deb
|
||||
|
||||
@@ -473,8 +473,10 @@ executable simplex-directory-service
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, composition ==1.0.*
|
||||
, containers ==0.6.*
|
||||
, crypton ==0.34.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, memory ==0.18.*
|
||||
, mtl >=2.3.1 && <3.0
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process >=1.6 && <1.6.18
|
||||
|
||||
@@ -62,7 +62,7 @@ import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Remote.AppVersion
|
||||
import Simplex.Chat.Remote.Types
|
||||
import Simplex.Chat.Stats (PresentedServersSummary)
|
||||
import Simplex.Chat.Store (AddressSettings, ChatLockEntity, GroupLink, GroupLinkInfo, StoreError (..), UserContactLink, UserMsgReceiptSettings)
|
||||
import Simplex.Chat.Store (AddressSettings, ChatLockEntity, GroupLinkInfo, StoreError (..), UserContactLink, UserMsgReceiptSettings)
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Chat.Types.Shared
|
||||
|
||||
@@ -6,7 +6,6 @@ module Simplex.Chat.Store
|
||||
ChatLockEntity (..),
|
||||
UserMsgReceiptSettings (..),
|
||||
UserContactLink (..),
|
||||
GroupLink (..),
|
||||
GroupLinkInfo (..),
|
||||
AddressSettings (..),
|
||||
AutoAccept (..),
|
||||
@@ -16,7 +15,6 @@ module Simplex.Chat.Store
|
||||
)
|
||||
where
|
||||
|
||||
import Simplex.Chat.Store.Groups (GroupLink (..))
|
||||
import Simplex.Chat.Store.Profiles
|
||||
import Simplex.Chat.Store.Shared
|
||||
import Simplex.Messaging.Agent.Store.Common (DBStore (..), withTransaction)
|
||||
|
||||
@@ -18,7 +18,6 @@ module Simplex.Chat.Store.Groups
|
||||
GroupInfoRow,
|
||||
GroupMemberRow,
|
||||
MaybeGroupMemberRow,
|
||||
GroupLink (..),
|
||||
toGroupInfo,
|
||||
toGroupMember,
|
||||
toMaybeGroupMember,
|
||||
@@ -162,7 +161,6 @@ import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Class
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Data.Bifunctor (second)
|
||||
import Data.Bitraversable (bitraverse)
|
||||
import Data.Char (toLower)
|
||||
@@ -188,7 +186,6 @@ import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..))
|
||||
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Ratchet (pattern PQEncOff, pattern PQSupportOff)
|
||||
import Simplex.Messaging.Parsers (defaultJSON)
|
||||
import Simplex.Messaging.Protocol (SubscriptionMode (..))
|
||||
import Simplex.Messaging.Util (eitherToMaybe, firstRow', safeDecodeUtf8, ($>>), ($>>=), (<$$>))
|
||||
import Simplex.Messaging.Version
|
||||
@@ -280,16 +277,6 @@ deleteGroupLink db User {userId} GroupInfo {groupId} = do
|
||||
(userId, groupId)
|
||||
DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND group_id = ?" (userId, groupId)
|
||||
|
||||
data GroupLink = GroupLink
|
||||
{ userContactLinkId :: Int64,
|
||||
connLinkContact :: CreatedLinkContact,
|
||||
shortLinkDataSet :: Bool,
|
||||
shortLinkLargeDataSet :: BoolDef,
|
||||
groupLinkId :: GroupLinkId,
|
||||
acceptMemberRole :: GroupMemberRole
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
getGroupLink :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO GroupLink
|
||||
getGroupLink db User {userId} gInfo@GroupInfo {groupId} =
|
||||
ExceptT . firstRow toGroupLink (SEGroupLinkNotFound gInfo) $
|
||||
@@ -982,9 +969,12 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do
|
||||
search = maybe "" (map toLower) search_
|
||||
|
||||
getUserGroupsWithSummary :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfoSummary]
|
||||
getUserGroupsWithSummary db vr user _contactId_ search_ =
|
||||
getUserGroupDetails db vr user _contactId_ search_
|
||||
>>= mapM (\g@GroupInfo {groupId} -> GIS g <$> getGroupSummary db user groupId)
|
||||
getUserGroupsWithSummary db vr user _contactId_ search_ = do
|
||||
gs <- getUserGroupDetails db vr user _contactId_ search_
|
||||
forM gs $ \g@GroupInfo {groupId} -> do
|
||||
s <- getGroupSummary db user groupId
|
||||
link_ <- eitherToMaybe <$> runExceptT (getGroupLink db user g)
|
||||
pure $ GIS g s link_
|
||||
|
||||
-- the statuses on non-current members should match memberCurrent' function
|
||||
getGroupSummary :: DB.Connection -> User -> GroupId -> IO GroupSummary
|
||||
@@ -2905,5 +2895,3 @@ updateGroupAlias db userId g@GroupInfo {groupId} localAlias = do
|
||||
updatedAt <- getCurrentTime
|
||||
DB.execute db "UPDATE groups SET local_alias = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (localAlias, updatedAt, userId, groupId)
|
||||
pure (g :: GroupInfo) {localAlias = localAlias}
|
||||
|
||||
$(J.deriveJSON defaultJSON ''GroupLink)
|
||||
|
||||
@@ -2204,8 +2204,6 @@ updateGroupScopeUnreadStats db vr user g@GroupInfo {membership} scopeInfo (unrea
|
||||
m_ <- runExceptT $ getGroupMemberById db vr user groupMemberId
|
||||
pure $ either (const m) id m_ -- Left shouldn't happen, but types require it
|
||||
|
||||
deriving instance Show BoolInt
|
||||
|
||||
setGroupChatItemsDeleteAt :: DB.Connection -> User -> GroupId -> [(ChatItemId, Int)] -> UTCTime -> IO [(ChatItemId, UTCTime)]
|
||||
setGroupChatItemsDeleteAt db User {userId} groupId itemIds currentTs = forM itemIds $ \(chatItemId, ttl) -> do
|
||||
let deleteAt = addUTCTime (realToFrac ttl) currentTs
|
||||
|
||||
@@ -527,7 +527,17 @@ data GroupSummary = GroupSummary
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data GroupInfoSummary = GIS {groupInfo :: GroupInfo, groupSummary :: GroupSummary}
|
||||
data GroupInfoSummary = GIS {groupInfo :: GroupInfo, groupSummary :: GroupSummary, groupLink :: Maybe GroupLink}
|
||||
deriving (Show)
|
||||
|
||||
data GroupLink = GroupLink
|
||||
{ userContactLinkId :: Int64,
|
||||
connLinkContact :: CreatedLinkContact,
|
||||
shortLinkDataSet :: Bool,
|
||||
shortLinkLargeDataSet :: BoolDef,
|
||||
groupLinkId :: GroupLinkId,
|
||||
acceptMemberRole :: GroupMemberRole
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data ContactOrGroup = CGContact Contact | CGGroup GroupInfo [GroupMember]
|
||||
@@ -2075,6 +2085,8 @@ $(JQ.deriveJSON defaultJSON ''Group)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''GroupSummary)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''GroupLink)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''GroupInfoSummary)
|
||||
|
||||
instance FromField MsgFilter where fromField = fromIntField_ msgFilterIntP
|
||||
|
||||
@@ -50,7 +50,7 @@ import Simplex.Chat.Operators
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Remote.AppVersion (AppVersion (..), pattern AppVersionRange)
|
||||
import Simplex.Chat.Remote.Types
|
||||
import Simplex.Chat.Store (AddressSettings (..), AutoAccept (..), GroupLink (..), StoreError (..), UserContactLink (..))
|
||||
import Simplex.Chat.Store (AddressSettings (..), AutoAccept (..), StoreError (..), UserContactLink (..))
|
||||
import Simplex.Chat.Styled
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Preferences
|
||||
@@ -1365,8 +1365,8 @@ viewGroupsList [] = ["you have no groups!", "to create: " <> highlight' "/g <nam
|
||||
viewGroupsList gs = map groupSS $ sortOn ldn_ gs
|
||||
where
|
||||
ldn_ :: GroupInfoSummary -> Text
|
||||
ldn_ (GIS GroupInfo {localDisplayName} _) = T.toLower localDisplayName
|
||||
groupSS (GIS g@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} GroupSummary {currentMembers}) =
|
||||
ldn_ (GIS GroupInfo {localDisplayName} _ _) = T.toLower localDisplayName
|
||||
groupSS (GIS g@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} GroupSummary {currentMembers} _) =
|
||||
case memberStatus membership of
|
||||
GSMemInvited -> groupInvitation' g
|
||||
s -> membershipIncognito g <> ttyFullGroup g <> viewMemberStatus s <> alias g
|
||||
|
||||
@@ -21,8 +21,7 @@ import Directory.Service
|
||||
import Directory.Store
|
||||
import GHC.IO.Handle (hClose)
|
||||
import Simplex.Chat.Bot.KnownContacts
|
||||
import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks)
|
||||
import Simplex.Chat.Core
|
||||
import Simplex.Chat.Controller (ChatConfig (..))
|
||||
import qualified Simplex.Chat.Markdown as MD
|
||||
import Simplex.Chat.Options (CoreChatOpts (..))
|
||||
import Simplex.Chat.Options.DB
|
||||
@@ -1129,11 +1128,12 @@ testListUserGroups promote ps =
|
||||
|
||||
checkListings :: [T.Text] -> [T.Text] -> IO ()
|
||||
checkListings listed promoted = do
|
||||
threadDelay 100000
|
||||
checkListing listingFileName listed
|
||||
checkListing promotedFileName promoted
|
||||
where
|
||||
checkListing f expected = do
|
||||
Just (DirectoryListing gs) <- J.decodeFileStrict $ "./tests/tmp/web" </> f
|
||||
Just (DirectoryListing gs) <- J.decodeFileStrict $ "./tests/tmp/web/data" </> f
|
||||
map groupName gs `shouldBe` expected
|
||||
groupName DirectoryEntry {displayName} = displayName
|
||||
|
||||
@@ -1396,14 +1396,9 @@ withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup webFolder test = do
|
||||
runDirectory :: ChatConfig -> DirectoryOpts -> IO () -> IO ()
|
||||
runDirectory cfg opts@DirectoryOpts {directoryLog} action = do
|
||||
st <- restoreDirectoryStore directoryLog
|
||||
t <- forkIO $ bot st
|
||||
t <- forkIO $ directoryService st opts cfg
|
||||
threadDelay 500000
|
||||
action `finally` (mapM_ hClose (directoryLogFile st) >> killThread t)
|
||||
where
|
||||
bot st = do
|
||||
env <- newServiceState opts
|
||||
let cfg' = cfg {chatHooks = defaultChatHooks {acceptMember = Just $ acceptMemberHook opts env}}
|
||||
simplexChatCore cfg' (mkChatOpts opts) $ directoryService st opts env
|
||||
|
||||
registerGroup :: TestCC -> TestCC -> String -> String -> IO ()
|
||||
registerGroup su u n fn = registerGroupId su u n fn 1 1
|
||||
|
||||
@@ -238,7 +238,7 @@
|
||||
"f-droid-page-simplex-chat-repo-section-text": "Chcete-li jej přidat do vašeho F-Droid clienta, <span class='hide-on-mobile'> naskenujte QR kód nebo</span> použijte tuto adresu URL:",
|
||||
"f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat a F-Droid.org repozitáře jsou podepsané různými klíči. Chcete-li přepnout, prosím <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>exportujte</a> chat databázi a přeinstalujte aplikaci.",
|
||||
"comparison-section-list-point-4a": "SimpleX relé nemůže ohrozit šifrování e2e. Ověřte bezpečnostní kód, který zmírňuje mimo pásmový útok na kanál",
|
||||
"docs-dropdown-8": "Služba SimpleX Directory",
|
||||
"docs-dropdown-8": "SimpleX Directory",
|
||||
"please-enable-javascript": "Prosím, povolte JavaScript k zobrazení QR kódu.",
|
||||
"please-use-link-in-mobile-app": "Prosím použijte odkaz v mobilní aplikaci",
|
||||
"simplex-chat-via-f-droid": "SimpleX Chat přes F-Droid",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"home": "Home",
|
||||
"directory": "Directory",
|
||||
"developers": "Developers",
|
||||
"reference": "Reference",
|
||||
"blog": "Blog",
|
||||
@@ -24,6 +25,7 @@
|
||||
"copyright-label": "© 2020-2025 SimpleX | Open-Source Project",
|
||||
"simplex-chat-protocol": "SimpleX Chat protocol",
|
||||
"terminal-cli": "Terminal CLI",
|
||||
"about-and-contact-us": "About & Contact us",
|
||||
"terms-and-privacy-policy": "Privacy Policy",
|
||||
"hero-header": "Privacy redefined",
|
||||
"hero-subheader": "The first messenger<br>without user IDs",
|
||||
@@ -232,7 +234,7 @@
|
||||
"docs-dropdown-5": "Host XFTP Server",
|
||||
"docs-dropdown-6": "WebRTC servers",
|
||||
"docs-dropdown-7": "Translate SimpleX Chat",
|
||||
"docs-dropdown-8": "SimpleX Directory Service",
|
||||
"docs-dropdown-8": "SimpleX Directory",
|
||||
"docs-dropdown-9": "Downloads",
|
||||
"docs-dropdown-10": "Transparency",
|
||||
"docs-dropdown-11": "FAQ",
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
"stable-versions-built-by-f-droid-org": "Stabiele versies gebouwd door F-Droid.org",
|
||||
"releases-to-this-repo-are-done-1-2-days-later": "De releases voor deze repository vinden enkele dagen later plaats",
|
||||
"f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat- en F-Droid.org-repository's ondertekenen builds met de verschillende sleutels. Om over te stappen, alstublieft <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>exporteer</a> de chatdatabase en installeer de app opnieuw.",
|
||||
"docs-dropdown-8": "SimpleX Directory Service",
|
||||
"docs-dropdown-8": "SimpleX Directory",
|
||||
"comparison-section-list-point-4a": "SimpleX relais kunnen de e2e-versleuteling niet in gevaar brengen. Controleer de beveiligingscode om aanvallen op out-of-band kanalen te beperken",
|
||||
"hero-overlay-3-title": "Beveiligings beoordeling",
|
||||
"hero-overlay-card-3-p-2": "Trail of Bits heeft in november 2022 de cryptografie en netwerkcomponenten van het SimpleX-platform beoordeeld. Lees meer in <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">de aankondiging</a>.",
|
||||
|
||||
@@ -172,6 +172,7 @@
|
||||
"simplex-private-card-10-point-2": "Это позволяет доставлять сообщения без идентификаторов профиля пользователя, и обеспечивает лучшую конфиденциальность метаданных, чем альтернативы.",
|
||||
"privacy-matters-2-title": "Манипулирование выборами",
|
||||
"home": "Главная",
|
||||
"directory": "Каталог",
|
||||
"chat-protocol": "Протокол чата",
|
||||
"simplex-private-card-5-point-2": "Это делает сообщениям разного размера одинаковыми для серверов и сети.",
|
||||
"hero-overlay-card-1-p-1": "Многие спрашивают: <em>Если у SimpleX нет никаких идентификаторов пользователя, то как приложение знает, куда доставлять сообщения?</em>",
|
||||
@@ -217,7 +218,7 @@
|
||||
"hero-2-header-desc": "В видео показано, как подключиться к Вашему другу через одноразовый QR-код, при встрече или во время видеосвязи. Вы также можете соединится, поделившись ссылкой-приглашением.",
|
||||
"simplex-network-overlay-card-1-li-6": "Сети P2P могут быть уязвимы для <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>DRDoS атаки</a>, когда клиенты могут ретранслировать и увеличивать трафик, что приводит к отказу всей сети. Клиенты SimpleX ретранслируют трафик только из известного соединения и не могут быть использованы злоумышленником для создания трафика во всей сети.",
|
||||
"if-you-already-installed-simplex-chat-for-the-terminal": "Если Вы уже установили SimpleX Chat для терминала",
|
||||
"docs-dropdown-8": "Служба Каталогов SimpleX",
|
||||
"docs-dropdown-8": "Каталог SimpleX",
|
||||
"simplex-private-card-1-point-1": "Протокол двойного обновления ключей —<br>\"отрицаемые\" сообщения с идеальной прямой секретностью и восстановлением после взлома.",
|
||||
"simplex-private-card-8-point-1": "Серверы SimpleX действуют как узлы-миксеры с низкой задержкой — входящие и исходящие сообщения имеют разный порядок.",
|
||||
"simplex-unique-overlay-card-2-p-1": "Поскольку у Вас нет идентификатора в сети SimpleX, никто не сможет связаться с Вами, если Вы сами не предоставите одноразовый или временный адрес в виде QR-кода или ссылки.",
|
||||
@@ -235,6 +236,7 @@
|
||||
"hero-overlay-card-1-p-6": "Подробнее читайте в <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>техническом описании SimpleX</a>.",
|
||||
"simplex-network-overlay-card-1-p-1": "Протоколы и приложения для обмена сообщениями <a href='https://ru.wikipedia.org/wiki/Peer-to-peer'>P2P</a> имеют различные проблемы, которые делают их менее надежными, чем SimpleX, более сложными для анализа и уязвимыми для нескольких типов атак.",
|
||||
"terms-and-privacy-policy": "Политика Конфиденциальности",
|
||||
"about-and-contact-us": "O нас и наши контакты",
|
||||
"simplex-network-overlay-card-1-li-1": "Сети P2P используют <a href='https://ru.wikipedia.org/wiki/Distributed_hash_table'>DHT</a> (распределенные хэш-таблицы) для маршрутизации сообщений. DHT должны обеспечивать баланс между гарантией доставки и задержкой. SimpleX имеет как лучшую гарантию доставки, так и меньшую задержку, чем P2P. В сетях P2P сообщение передается через нескольких узлов, последовательно, кол-во узлов-посредников будет расти параллельно размеру сети — <em>O(log N)</em>.",
|
||||
"privacy-matters-section-label": "Убедитесь, что Ваш мессенджер не может получить доступ к Вашим данным!",
|
||||
"simplex-unique-overlay-card-3-p-1": "SimpleX Chat хранит все пользовательские данные на клиентских устройствах в <strong>портативном формате зашифрованной базы данных</strong> которую можно перенести на другое устройство.",
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
cd ..
|
||||
./website/web.sh
|
||||
cd website
|
||||
npm run start
|
||||
@@ -7,6 +7,26 @@
|
||||
<a href="/{{ '' if lang == 'en' else lang }}" class="h-full mb-14 hidden dark:inline-block"><img class="w-auto h-[46px]" src="/img/new/logo-dark.png" alt="logo" /></a>
|
||||
|
||||
<div class="flex flex-col items-center lg:items-start lg:flex-row gap-[150px] lg:gap-[350px]">
|
||||
<div class="flex flex-col items-center lg:items-start">
|
||||
<a href="/about/"
|
||||
class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1">
|
||||
{{ "about-and-contact-us" | i18n({}, lang ) | safe }}
|
||||
</a>
|
||||
<a href="/privacy/"
|
||||
class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1">
|
||||
{{ "terms-and-privacy-policy" | i18n({}, lang ) | safe }}
|
||||
</a>
|
||||
<a href="/jobs/"
|
||||
class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1">
|
||||
{{ "jobs" | i18n({}, lang ) | safe }}
|
||||
</a>
|
||||
<a href="https://github.com/simplex-chat/simplex-chat#please-support-us-with-your-donations"
|
||||
target="_blank"
|
||||
class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1">
|
||||
{{ "donate" | i18n({}, lang ) | safe }}
|
||||
<svg class="float-right" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-col items-center lg:items-start">
|
||||
<a href="https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md"
|
||||
target="_blank"
|
||||
@@ -20,28 +40,16 @@
|
||||
{{ "simplex-chat-protocol" | i18n({}, lang ) | safe }}
|
||||
<svg class="float-right" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg>
|
||||
</a>
|
||||
<a href="/privacy"
|
||||
class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1">
|
||||
{{ "terms-and-privacy-policy" | i18n({}, lang ) | safe }}
|
||||
</a>
|
||||
<a href="https://github.com/simplex-chat/simplex-chat#please-support-us-with-your-donations"
|
||||
target="_blank"
|
||||
class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1">
|
||||
{{ "donate" | i18n({}, lang ) | safe }}
|
||||
<svg class="float-right" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-col items-center lg:items-start">
|
||||
<a href="https://github.com/simplex-chat/simplex-chat/blob/stable/docs/CLI.md"
|
||||
target="_blank"
|
||||
class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1">
|
||||
{{ "terminal-cli" | i18n({}, lang ) | safe }}
|
||||
<svg class="float-right" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg>
|
||||
</a>
|
||||
<a href="https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript"
|
||||
<a href="https://github.com/simplex-chat/simplex-chat/tree/stable/bots"
|
||||
target="_blank"
|
||||
class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1">
|
||||
TypeScript SDK
|
||||
SimpleX bots API
|
||||
<svg class="float-right" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg>
|
||||
</a>
|
||||
</div>
|
||||
@@ -56,7 +64,7 @@
|
||||
<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://snort.social/p/npub1exv22uulqnmlluszc4yk92jhs2e5ajcs6mu3t00a6avzjcalj9csm7d828" target="_blank">
|
||||
<a href="https://primal.net/p/npub1exv22uulqnmlluszc4yk92jhs2e5ajcs6mu3t00a6avzjcalj9csm7d828" target="_blank">
|
||||
<svg class="fill-primary-light dark:fill-primary-dark" width="32" height="32" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M256 512C397.385 512 512 397.385 512 256C512 114.615 397.385 0 256 0C114.615 0 0 114.615 0 256C0 397.385 114.615 512 256 512ZM348.077 268.251C347.047 271.254 346.018 274.257 344.263 277.017C337.633 287.302 327.867 292.139 315.221 292.951C302.173 293.789 289.233 295.487 276.333 297.437C274.064 297.779 271.793 298.108 269.521 298.438C258.687 300.005 247.858 301.572 237.36 304.645C189.946 318.517 148.927 341.344 115.048 374.257C109.197 379.941 103.451 385.688 98.1836 391.819C95.7632 394.637 93.0278 397.201 89.1831 398.521L89.1304 398.538C87.9731 398.938 86.7466 399.359 85.6548 398.491C84.7729 397.79 84.9731 396.874 85.1665 395.992C85.1948 395.862 85.2231 395.733 85.248 395.605C85.6963 393.277 86.7407 391.14 88.1821 389.194C90.146 386.544 92.084 383.878 94.022 381.213C99.7344 373.356 105.446 365.5 111.82 358.036C121.707 346.457 133.167 336.2 144.787 326.072C168.56 305.352 195.75 289.087 225.833 276.646C228.757 275.438 231.732 274.327 234.707 273.217C236.528 272.538 238.349 271.858 240.158 271.155C240.361 271.076 240.574 271.007 240.789 270.938C241.635 270.663 242.518 270.377 243.024 269.423C242.177 268.706 241.15 268.783 240.177 268.857C239.968 268.873 239.762 268.889 239.561 268.896C217.297 269.744 195.694 273.47 174.884 280.725C167.117 283.432 159.668 286.729 152.295 290.172C149.855 291.311 147.46 292.248 145.208 290.121C142.917 287.958 143.432 285.598 144.912 283.182C154.453 267.61 165.746 253.295 179.889 240.66C197.997 224.483 219.395 213.954 244.032 208.49C276.996 201.18 306.323 208.416 332.639 226.84L333.331 227.326C337.818 230.48 342.154 233.529 348.424 232.938C356.094 232.213 362.589 229.362 367.853 224.391C376.977 215.774 378.435 205.5 372.124 194.314C365.358 182.325 356.674 171.414 347.996 160.512L347.97 160.479C344.757 156.442 341.94 152.257 340.075 147.559C337.776 141.771 339.282 136.798 343.771 132.374C346.562 129.623 349.879 127.444 353.499 125.654C352.84 124.302 351.792 124.372 350.828 124.437C350.692 124.445 350.558 124.454 350.427 124.459C349.885 124.479 349.345 124.565 348.806 124.651C348.214 124.745 347.624 124.839 347.031 124.847C346.607 124.852 346.152 124.89 345.689 124.929C343.945 125.074 342.084 125.23 341.357 123.684C340.391 121.631 341.399 119.227 343.191 117.554C347.403 113.623 352.741 112.595 358.621 113.133C364.851 113.703 370.044 116.374 374.959 119.561C377.904 121.469 380.753 123.504 383.548 125.587C386.107 127.494 385.938 128.352 382.863 129.358C378.374 130.828 373.854 132.237 369.158 133.088C368.043 133.29 366.97 133.568 365.925 133.915C365.13 134.178 364.353 134.481 363.586 134.82C356.43 137.985 354.645 142.229 357.784 148.742C360.525 154.429 364.434 159.469 368.344 164.509C369.887 166.498 371.431 168.487 372.902 170.517C373.603 171.483 374.306 172.449 375.008 173.415C379.468 179.541 383.926 185.666 387.914 192.062C400.479 212.217 392.457 231.471 374.209 243.009C372.334 244.194 370.384 245.277 368.434 246.36C365.527 247.975 362.621 249.589 359.962 251.543C354.495 255.563 350.765 260.588 348.648 266.601C348.455 267.149 348.266 267.7 348.077 268.251ZM364.513 333.922C365.149 335.249 364.924 336.668 364.888 338.06C364.714 340.459 365.214 342.489 363.518 344.059C362.183 344.074 361.458 343.352 360.753 342.65C360.563 342.461 360.375 342.273 360.177 342.103C356.286 338.75 352.875 334.988 348.808 331.769C341.752 326.184 333.477 324.483 324.302 324.521C315.342 324.559 307.177 327.164 299.004 329.771C297.717 330.183 296.429 330.594 295.138 330.995L289.747 332.666C278.586 336.121 267.42 339.577 256.422 343.413C246.833 346.757 238.413 339.996 239.31 331.924C240.31 322.924 243.14 314.443 250.956 307.944C254.783 304.762 259.324 303.146 264.323 302.431C267.762 301.938 271.221 301.553 274.679 301.168C276.062 301.014 277.446 300.859 278.828 300.698C279.451 300.625 280.078 300.607 280.639 301.144C280.923 302.208 280.246 302.979 279.585 303.731C279.429 303.91 279.273 304.088 279.131 304.268C274.998 309.516 270.639 314.606 265.876 319.418C265.727 319.568 265.549 319.722 265.365 319.879C264.61 320.528 263.763 321.258 264.454 322.215C265.352 323.459 266.887 322.692 268.166 322.415C273.744 321.205 279.287 319.864 284.829 318.523C294.818 316.107 304.808 313.69 315.003 312.043C328.089 309.928 340.422 311.512 351.549 318.553C357.728 322.463 361.604 327.85 364.513 333.922Z" />
|
||||
</svg>
|
||||
|
||||
@@ -12,15 +12,23 @@
|
||||
<a href="/{{ '' if lang == 'en' else lang }}" class="h-full hidden dark:lg:flex items-center ltr:mr-auto rtl:ml-auto"><img class="w-auto max-h-[50px] pr-10" src="/img/new/logo-dark.png" alt="logo" /></a>
|
||||
<a href="/{{ '' if lang == 'en' else lang }}" class="dark:hidden lg:hidden ltr:mr-auto rtl:ml-auto"><img class="h-[32px]" src="/img/new/logo-symbol-light.svg" alt="" srcset=""></a>
|
||||
<a href="/{{ '' if lang == 'en' else lang }}" class="hidden dark:inline-block dark:lg:hidden lg:hidden ltr:mr-auto rtl:ml-auto"><img class="h-[32px]" src="/img/new/logo-symbol-dark.svg" alt="" srcset=""></a>
|
||||
|
||||
|
||||
<nav class="bg-[#F0F1F2] dark:bg-gradient-radial-mobile dark:lg:bg-none lg:bg-transparent fixed top-[66px] left-0 right-0 bottom-0 h-[calc(100vh-66px)] lg:h-[66px] lg:top-0 lg:relative" id="menu">
|
||||
<div class="flex flex-col lg:flex-row justify-between lg:items-center gap-5 xl:gap-10 px-4 lg:px-0 h-full">
|
||||
<ul class="flex flex-col lg:flex-row lg:items-center gap-3 py-4 lg:py-0 lg:gap-5 xl:gap-8">
|
||||
|
||||
|
||||
<li class="nav-link relative {% if active_home %}active{% endif %}">
|
||||
<a href="/{{ '' if lang == 'en' else lang }}" class="flex items-center justify-between gap-2 lg:py-5 whitespace-nowrap">
|
||||
<span class="text-[16px] leading-[26px] tracking-[0.01em] nav-link-text text-black dark:text-white before:bg-black dark:before:bg-white">{{ "home" | i18n({}, lang ) | safe }}</span>
|
||||
</a>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<hr class="dark:opacity-[0.1]" >
|
||||
|
||||
<li class="nav-link relative {% if active_directory %}active{% endif %}">
|
||||
<a href="/directory/" class="flex items-center justify-between gap-2 lg:py-5 whitespace-nowrap">
|
||||
<span class="text-[16px] leading-[26px] tracking-[0.01em] nav-link-text text-black dark:text-white before:bg-black dark:before:bg-white">{{ "directory" | i18n({}, lang ) | safe }}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<hr class="dark:opacity-[0.1]" >
|
||||
@@ -34,7 +42,7 @@
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
|
||||
<ul class="flex flex-col items-start gap-2 lg:h-fit lg:absolute lg:bg-white dark:lg:bg-black top-full lg:mt-[10px] lg:py-4 min-w-[180px] rounded-md lg:shadow-[0_0_3px_rgb(60_72_88_/_15%)] sub-menu">
|
||||
{% for guide in guide_dropdown.items %}
|
||||
<li><a href="{{ guide.url | url }}" class="{{ "active" if page.url == guide.url | url else '' }} lg:px-[20px] inline-block whitespace-nowrap">{{ guide.title | i18n({}, lang ) | safe }}</a></li>
|
||||
@@ -53,7 +61,7 @@
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
|
||||
<ul class="flex flex-col items-start gap-2 lg:h-fit lg:absolute lg:bg-white dark:lg:bg-black top-full lg:mt-[10px] lg:py-4 min-w-[180px] rounded-md lg:shadow-[0_0_3px_rgb(60_72_88_/_15%)] sub-menu">
|
||||
<li><a href="https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md"
|
||||
target="_blank" class="lg:px-[20px] flex items-center gap-1"
|
||||
@@ -71,9 +79,9 @@
|
||||
>{{ "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]">
|
||||
|
||||
|
||||
{% for doc in docs_dropdown.items %}
|
||||
<li><a href="{{ doc.url | url }}" class="{{ "active" if page.url == doc.url | url else '' }} lg:px-[20px] inline-block whitespace-nowrap">{{ doc.title | i18n({}, lang ) | safe }}</a></li>
|
||||
{% endfor %}
|
||||
@@ -110,14 +118,6 @@
|
||||
|
||||
<hr class="dark:opacity-[0.1]" >
|
||||
|
||||
<li class="nav-link relative {% if active_jobs %}active{% endif %}">
|
||||
<a href="/jobs" class="flex items-center justify-between gap-2 lg:py-5 whitespace-nowrap">
|
||||
<span class="text-[16px] leading-[26px] tracking-[0.01em] nav-link-text text-black dark:text-white before:bg-black dark:before:bg-white">{{ "jobs" | i18n({}, lang ) | safe }}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<hr class="dark:opacity-[0.1]" >
|
||||
|
||||
<li class="nav-link relative {% if active_blog %}active{% endif %}">
|
||||
<a href="/blog" class="flex items-center justify-between gap-2 lg:py-5">
|
||||
<span class="text-[16px] leading-[26px] tracking-[0.01em] nav-link-text text-black dark:text-white before:bg-black dark:before:bg-white">{{ "blog" | i18n({}, lang ) | safe }}</span>
|
||||
@@ -143,7 +143,7 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% if ('blog' not in page.url) and ('jobs' not in page.url) %}
|
||||
{% if ('blog' not in page.url) and ('jobs' not in page.url) and ('about' not in page.url) and ('privacy' not in page.url) and ('directory' not in page.url) %}
|
||||
<div class="nav-link relative flag-container">
|
||||
<a href="javascript:void(0);" class="flex items-center justify-end ltr:ml-8 ltr:lg:ml-5 ltr:xl:ml-10 rtl:mr-8 rtl:lg:mr-5 rtl:xl:mr-10 h-6 w-8 whitespace-nowrap">
|
||||
{% for language in languages.languages %}
|
||||
@@ -158,7 +158,7 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</a>
|
||||
|
||||
|
||||
<ul class="flex flex-col items-start gap-2 h-fit absolute top-11 -left-10 bg-white dark:bg-black mt-[10px] py-4 min-w-[170px] rounded-md shadow-[0_0_3px_rgb(60_72_88_/_15%)] sub-menu overflow-auto">
|
||||
{% if ("docs" in page.url) or ('downloads' in page.url) %}
|
||||
{% for supportedLang in supportedLangsForDoc %}
|
||||
@@ -177,7 +177,7 @@
|
||||
</div>
|
||||
<p>{{ language.name }}</p>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
@@ -198,7 +198,7 @@
|
||||
</div>
|
||||
<p>{{ language.name }}</p>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
+70
-58
@@ -1,36 +1,42 @@
|
||||
---
|
||||
layout: layouts/main.html
|
||||
title: "SimpleX blog: the latest news"
|
||||
description: "SimpleX Chat - a private and encrypted messenger without any user IDs (not even random ones)! Make a private connection via link / QR code to send messages and make calls."
|
||||
description: "SimpleX Chat - a private and encrypted messenger without any user IDs (not even random ones)! Make a
|
||||
private connection via link / QR code to send messages and make calls."
|
||||
path: /blog
|
||||
templateEngineOverride: njk
|
||||
active_blog: true
|
||||
---
|
||||
{% block css_links %}
|
||||
<style>
|
||||
#blog-list ul li {
|
||||
list-style: disc;
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
#blog-list ul {
|
||||
list-style-position: inside;
|
||||
overflow: auto;
|
||||
}
|
||||
#blog-list ul li {
|
||||
-webkit-margin-start: 1.1rem;
|
||||
color: #000;
|
||||
}
|
||||
.dark #blog-list ul li {
|
||||
color: #fff;
|
||||
}
|
||||
#blog-list ul li::marker {
|
||||
color: black;
|
||||
}
|
||||
.dark #blog-list ul li::marker {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
#blog-list ul li {
|
||||
list-style: disc;
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#blog-list ul {
|
||||
list-style-position: inside;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#blog-list ul li {
|
||||
-webkit-margin-start: 1.1rem;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.dark #blog-list ul li {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#blog-list ul li::marker {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.dark #blog-list ul li::marker {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
<section class="py-10 px-5 mt-[66px]" id="blog-list">
|
||||
@@ -39,42 +45,48 @@ active_blog: true
|
||||
|
||||
{% for blog in collections.blogs %}
|
||||
{% if not(blog.data.draft) %}
|
||||
<article class="w-full flex flex-col items-start md:flex-row rounded-[4px] overflow-hidden shadow-[0px_20px_30px_rgba(0,0,0,0.12)] dark:shadow-none bg-white dark:bg-[#11182F] mb-8">
|
||||
<div class="min-h-[200px] h-[inherit] self-stretch md:w-[168px] bg-[#D9E7ED] dark:bg-[#17203D] flex items-center justify-center flex-[1] relative">
|
||||
<div class="min-h-[inherit] h-full w-full flex items-end px-4 pt-4 justify-center relative">
|
||||
{% if blog.data.image %}
|
||||
{% if blog.data.imageBottom %}
|
||||
<img class="w-full max-w-[240px] h-auto" src="{{ blog.data.image }}" alt="" srcset=""/>
|
||||
{% elif blog.data.imageWide %}
|
||||
<img class="mb-4 self-center w-full h-auto" src="{{ blog.data.image }}" alt="" srcset=""/>
|
||||
{% else %}
|
||||
<img class="mb-4 self-center w-full max-w-[240px] h-auto" src="{{ blog.data.image }}" alt="" srcset=""/>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<img class="h-[44px] self-center dark:hidden" src="/img/new/logo-symbol-light.svg" alt="" srcset=""/>
|
||||
<img class="h-[44px] self-center hidden dark:inline-block" src="/img/new/logo-symbol-dark.svg" alt="" srcset=""/>
|
||||
{% endif %}
|
||||
</div>
|
||||
<article
|
||||
class="w-full flex flex-col items-start md:flex-row rounded-[4px] overflow-hidden shadow-[0px_20px_30px_rgba(0,0,0,0.12)] dark:shadow-none bg-white dark:bg-[#11182F] mb-8">
|
||||
<div
|
||||
class="min-h-[200px] h-[inherit] self-stretch md:w-[168px] bg-[#D9E7ED] dark:bg-[#17203D] flex items-center justify-center flex-[1] relative">
|
||||
<div class="min-h-[inherit] h-full w-full flex items-end px-4 pt-4 justify-center relative">
|
||||
{% if blog.data.image %}
|
||||
{% if blog.data.imageBottom %}
|
||||
<img class="w-full max-w-[240px] h-auto" src="{{ blog.data.image }}" alt="" srcset="" />
|
||||
{% elif blog.data.imageWide %}
|
||||
<img class="mb-4 self-center w-full h-auto" src="{{ blog.data.image }}" alt="" srcset="" />
|
||||
{% else %}
|
||||
<img class="mb-4 self-center w-full max-w-[240px] h-auto" src="{{ blog.data.image }}" alt=""
|
||||
srcset="" />
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<img class="h-[44px] self-center dark:hidden" src="/img/new/logo-symbol-light.svg" alt=""
|
||||
srcset="" />
|
||||
<img class="h-[44px] self-center hidden dark:inline-block" src="/img/new/logo-symbol-dark.svg"
|
||||
alt="" srcset="" />
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="p-6 md:py-8 flex-[2.5] flex flex-col">
|
||||
<div>
|
||||
<h1 class="text-grey-black dark:text-white !text-lg md:!text-xl font-bold ">
|
||||
<a href="{{ blog.url }}">{{ blog.data.title | safe }}</a>
|
||||
</h1>
|
||||
<p class="text-sm text-[#A8B0B4] font-medium mt-2 mb-4 tracking-[0.03em]">
|
||||
{{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }}
|
||||
</p>
|
||||
{% if blog.data.previewBody %}
|
||||
<div class="mb-4 dark:text-white">
|
||||
{% include blog.data.previewBody %}
|
||||
</div>
|
||||
{% elif blog.data.preview %}
|
||||
<p class="dark:text-white mb-4">{{ blog.data.preview | safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="p-6 md:py-8 flex-[2.5] flex flex-col">
|
||||
<div>
|
||||
<h2 class="text-grey-black dark:text-white !text-lg md:!text-xl font-bold">
|
||||
<a href="{{ blog.url }}">{{ blog.data.title | safe }}</a>
|
||||
</h2>
|
||||
<p class="text-sm text-[#A8B0B4] font-medium mt-2 mb-4 tracking-[0.03em]">
|
||||
{{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }}
|
||||
</p>
|
||||
{% if blog.data.previewBody %}
|
||||
<div class="mb-4 dark:text-white">
|
||||
{% include blog.data.previewBody %}
|
||||
</div>
|
||||
<a class="block text-primary-light dark:text-[#70F0F9] text-base font-medium tracking-[0.03em] mt-auto" href="{{ blog.url }}">Read More</a>
|
||||
{% elif blog.data.preview %}
|
||||
<p class="dark:text-white mb-4">{{ blog.data.preview | safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
<a class="block text-primary-light dark:text-[#70F0F9] text-base font-medium tracking-[0.03em] mt-auto"
|
||||
href="{{ blog.url }}">Read More</a>
|
||||
</div>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
+246
-78
@@ -210,6 +210,11 @@
|
||||
#3f5598 70%,
|
||||
#c3faff 90%,
|
||||
#fff6e0 100%);
|
||||
--sa-top: env(safe-area-inset-top, 0px);
|
||||
--sa-right: env(safe-area-inset-right, 0px);
|
||||
--sa-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--sa-left: env(safe-area-inset-left, 0px);
|
||||
--vh: 1svh;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -221,7 +226,12 @@
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
font-family: GT-Walsheim, Gilroy, Helvetica, sans-serif;
|
||||
letter-spacing: 0.003em;
|
||||
}
|
||||
|
||||
html, body { background:#000; }
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -231,74 +241,11 @@ img {
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
a {
|
||||
a,
|
||||
p {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* NEW SITE */
|
||||
.container,
|
||||
.container-fluid,
|
||||
.container-xxl,
|
||||
.container-xl,
|
||||
.container-lg,
|
||||
.container-md,
|
||||
.container-sm {
|
||||
width: 100%;
|
||||
/* padding: 0 20px; */
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
|
||||
.container-sm,
|
||||
.container {
|
||||
max-width: 540px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
||||
.container-md,
|
||||
.container-sm,
|
||||
.container {
|
||||
max-width: 720px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
|
||||
.container-lg,
|
||||
.container-md,
|
||||
.container-sm,
|
||||
.container {
|
||||
max-width: 960px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
|
||||
.container-xl,
|
||||
.container-lg,
|
||||
.container-md,
|
||||
.container-sm,
|
||||
.container {
|
||||
max-width: 1140px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
|
||||
.container-xxl,
|
||||
.container-xl,
|
||||
.container-lg,
|
||||
.container-md,
|
||||
.container-sm,
|
||||
.container {
|
||||
max-width: 1320px;
|
||||
}
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(90deg,
|
||||
#019bfe 0%,
|
||||
@@ -311,17 +258,39 @@ a {
|
||||
}
|
||||
|
||||
.screen {
|
||||
height: 100vh;
|
||||
overflow-y: scroll;
|
||||
height: 100dvh;
|
||||
/* visible height, updates as bars expand/collapse */
|
||||
height: calc(var(--vh) * 100);
|
||||
/* fallback for older browsers */
|
||||
overflow-y: auto;
|
||||
scroll-snap-type: y mandatory;
|
||||
/* page-by-page snap */
|
||||
scroll-behavior: smooth;
|
||||
overscroll-behavior: contain;
|
||||
/* avoid rubber-banding pulling UI over content */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
section {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
height: 100dvh;
|
||||
height: calc(var(--vh) * 100);
|
||||
/* same fallback */
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always;
|
||||
/* requires a full swipe/scroll to leave */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Foreground content sits inside the safe area without changing section height */
|
||||
section .content {
|
||||
min-height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding:
|
||||
max(16px, var(--sa-top)) max(16px, var(--sa-right)) max(24px, var(--sa-bottom))
|
||||
/* lifts content above the iOS bottom bar */
|
||||
max(16px, var(--sa-left));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* --- COVER --- */
|
||||
@@ -369,6 +338,30 @@ section.cover div.content {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.cover .socials {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
bottom: 32px;
|
||||
padding: 0 4vw;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cover .socials a img {
|
||||
width: auto;
|
||||
height: 40px;
|
||||
border: #4f4f4f 1px solid;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.cover .socials a img.no-border {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* --- MAIN SECTIONS --- */
|
||||
main {
|
||||
position: relative;
|
||||
@@ -414,6 +407,10 @@ main section .image {
|
||||
background-image: url("/img/design_3/token-31-04_webp.webp");
|
||||
}
|
||||
|
||||
.page-6 .image {
|
||||
background-image: url("/img/design_3/section-6-desktop.webp");
|
||||
}
|
||||
|
||||
.text-container {
|
||||
position: relative;
|
||||
color: white;
|
||||
@@ -422,23 +419,26 @@ main section .image {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 2.5rem;
|
||||
margin-right: auto;
|
||||
padding-left: 8rem;
|
||||
gap: 2.3vh;
|
||||
margin-right: 6.25vw;
|
||||
margin-left: 6.25vw;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.text-container h2 {
|
||||
font-family: "GT-Walsheim", "Manrope", sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: clamp(2.5rem, 8vw, 5.6rem);
|
||||
font-size: 4.6vw;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.05;
|
||||
max-width: 24vw;
|
||||
}
|
||||
|
||||
.text-container p {
|
||||
font-family: "Manrope", "GT-Walsheim", sans-serif;
|
||||
font-weight: 200;
|
||||
font-size: clamp(1rem, 2.25vw, 1.84rem);
|
||||
font-size: 1.45vw;
|
||||
max-width: 20.5vw;
|
||||
}
|
||||
|
||||
.text-container p span {
|
||||
@@ -448,15 +448,183 @@ main section .image {
|
||||
.text-container a {
|
||||
font-family: "Manrope", "GT-Walsheim", sans-serif;
|
||||
font-weight: 200;
|
||||
font-size: clamp(1rem, 2.25vw, 1.84rem);
|
||||
font-size: 1.45vw;
|
||||
text-decoration: underline;
|
||||
text-decoration-line: underline;
|
||||
text-decoration-thickness: 0.5px;
|
||||
text-underline-offset: 4px;
|
||||
max-width: 13vw;
|
||||
}
|
||||
|
||||
.page-2 .text-container h2 {
|
||||
max-width: 24vw !important;
|
||||
}
|
||||
|
||||
.page-2 .text-container p {
|
||||
max-width: 20.5vw;
|
||||
}
|
||||
|
||||
.page-3 .text-container h2 {
|
||||
max-width: 30vw !important;
|
||||
}
|
||||
|
||||
.page-3 .text-container p {
|
||||
max-width: 28.5vw;
|
||||
}
|
||||
|
||||
.page-3 .text-container,
|
||||
.page-4 .text-container {
|
||||
margin-right: 8rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.page-4 .text-container h2 {
|
||||
max-width: 28vw !important;
|
||||
}
|
||||
|
||||
.page-4 .text-container p {
|
||||
max-width: 26.5vw;
|
||||
}
|
||||
|
||||
.page-4 .text-container a {
|
||||
max-width: 26.5vw;
|
||||
}
|
||||
|
||||
.roadmap>p:first-child {
|
||||
font-weight: 500;
|
||||
color: #64fdff;
|
||||
}
|
||||
|
||||
.roadmap>p:nth-child(2) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.roadmap>p:nth-child(3) {
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.roadmap p {
|
||||
max-width: 30vw;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
section {
|
||||
min-height: 100svh;
|
||||
}
|
||||
|
||||
section.cover div.content {
|
||||
gap: 35px;
|
||||
}
|
||||
|
||||
.cover .content h1 {
|
||||
font-size: 30vw;
|
||||
}
|
||||
|
||||
.cover .content h2 {
|
||||
font-size: 7vw;
|
||||
}
|
||||
|
||||
.cover .content p {
|
||||
font-size: 4.3vw;
|
||||
max-width: 62.5vw;
|
||||
}
|
||||
|
||||
.cover .socials {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* --- MAIN SECTIONS --- */
|
||||
.page-2 .image {
|
||||
background-image: url("/img/design_3/section2-mobile-bg.webp");
|
||||
}
|
||||
|
||||
.page-3 .image {
|
||||
background-image: url("/img/design_3/section-3-mobile.webp");
|
||||
}
|
||||
|
||||
.page-4 .image {
|
||||
background-image: url("/img/design_3/section-4-mobile.webp");
|
||||
height: 55%;
|
||||
}
|
||||
|
||||
.page-6 .image {
|
||||
background-image: url("/img/design_3/section-6-mobile.webp");
|
||||
height: 48%;
|
||||
}
|
||||
|
||||
.page-3 .text-container,
|
||||
.page-4 .text-container {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
gap: 1.25rem;
|
||||
margin-right: 8vw !important;
|
||||
margin-left: 8vw !important;
|
||||
padding-bottom: 8vw;
|
||||
max-width: 75vw;
|
||||
}
|
||||
|
||||
.text-container h2 {
|
||||
font-family: "GT-Walsheim", "Manrope", sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 9.25vw;
|
||||
letter-spacing: -0.025em;
|
||||
max-width: 75vw !important;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.text-container p {
|
||||
font-size: 4.2vw;
|
||||
}
|
||||
|
||||
.text-container a {
|
||||
font-weight: 300;
|
||||
font-size: 4.2vw;
|
||||
}
|
||||
|
||||
.text-container p,
|
||||
.text-container a {
|
||||
max-width: 74vw !important;
|
||||
}
|
||||
|
||||
.page-2 .text-container {
|
||||
max-width: 75vw;
|
||||
}
|
||||
|
||||
.page-2 .text-container h2 {
|
||||
max-width: 75vw !important;
|
||||
}
|
||||
|
||||
.page-3 .text-container {
|
||||
max-width: 80vw !important;
|
||||
}
|
||||
|
||||
.page-3 .text-container p {
|
||||
max-width: 80vw !important;
|
||||
}
|
||||
|
||||
.page-3 .text-container h2 {
|
||||
max-width: 85vw !important;
|
||||
font-size: 13vw;
|
||||
}
|
||||
|
||||
.page-4 .text-container {
|
||||
max-width: 80vw !important;
|
||||
|
||||
}
|
||||
|
||||
.page-4 .text-container h2 {
|
||||
max-width: 80vw !important;
|
||||
font-size: 14vw;
|
||||
}
|
||||
|
||||
.page-4 .text-container p {
|
||||
max-width: 80vw !important;
|
||||
}
|
||||
|
||||
.page-4 .text-container a {
|
||||
max-width: 80vw !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
---
|
||||
layout: layouts/main.html
|
||||
title: "SimpleX Directory"
|
||||
description: "Find communities on SimpleX network and create your own"
|
||||
templateEngineOverride: njk
|
||||
active_directory: true
|
||||
---
|
||||
|
||||
{% set lang = page.url | getlang %}
|
||||
{% block js_scripts %}
|
||||
<script src="/js/flag-anchor.js"></script>
|
||||
<script async defer src="/js/directory.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
<style>
|
||||
#directory .entry {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
#directory .entry a {
|
||||
order: -1;
|
||||
object-fit: cover;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#directory .entry a img {
|
||||
min-width: 104px;
|
||||
min-height: 104px;
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
#directory .entry h2 {
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
#directory .entry p {
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
#directory .entry .secret {
|
||||
filter: blur(5px);
|
||||
cursor: pointer;
|
||||
transition: filter 0.1s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#directory .entry .secret.visible {
|
||||
filter: none;
|
||||
user-select: auto;
|
||||
}
|
||||
|
||||
#directory .entry .read-more {
|
||||
color: #0053D0;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#directory .entry .read-less {
|
||||
color: darkgray;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dark #directory .entry .read-more {
|
||||
color: #70F0F9;
|
||||
}
|
||||
|
||||
#directory .entry .red {
|
||||
color: #DD0000;
|
||||
}
|
||||
|
||||
#directory .entry .green {
|
||||
color: #20BD3D;
|
||||
}
|
||||
|
||||
#directory .entry .blue {
|
||||
color: #0053d0;
|
||||
}
|
||||
|
||||
#directory .entry .cyan {
|
||||
color: #0AC4D1;
|
||||
}
|
||||
|
||||
#directory .entry .yellow {
|
||||
color: #DEBD00;
|
||||
}
|
||||
|
||||
#directory .entry .magenta {
|
||||
color: magenta;
|
||||
}
|
||||
|
||||
.dark #directory .entry .green {
|
||||
color: #4DDA67;
|
||||
}
|
||||
|
||||
.dark #directory .entry .blue {
|
||||
color: #00A2FF;
|
||||
}
|
||||
|
||||
.dark #directory .entry .cyan {
|
||||
color: #70F0F9;
|
||||
}
|
||||
|
||||
.dark #directory .entry .yellow {
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.dark #directory .entry .magenta {
|
||||
color: magenta;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 20px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
font-size: 14px;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.dark .pagination button {
|
||||
color: #70F0F9;
|
||||
}
|
||||
|
||||
.pagination button:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.dark .pagination button:hover {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
.pagination button.active {
|
||||
font-weight: bold;
|
||||
color: #11182F;
|
||||
}
|
||||
|
||||
.dark .pagination button.active {
|
||||
color: #70F0F9;
|
||||
}
|
||||
|
||||
.pagination button.text-btn {
|
||||
border-radius: 20px;
|
||||
min-width: auto;
|
||||
height: 40px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.pagination button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pagination {
|
||||
gap: 2px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
font-size: 12px;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.pagination button.text-btn {
|
||||
padding: 4px 8px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.pagination button:not(.text-btn):not(.active):not(.neighbor) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pagination button.active,
|
||||
.pagination button.neighbor {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#search {
|
||||
width: 100%;
|
||||
max-width: 540px;
|
||||
padding: 8px 12px 8px 32px;
|
||||
font-size: 15px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background-color: #f2f2f7;
|
||||
color: #000000;
|
||||
outline: none;
|
||||
transition: background-color 0.2s, box-shadow 0.2s;
|
||||
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZT0iIzg4ODg4OCI+CjxjaXJjbGUgY3g9IjEwLjUiIGN5PSIxMC41IiByPSI3LjUiIC8+CjxsaW5lIHgxPSIxNiIgeTE9IjE2IiB4Mj0iMjEiIHkyPSIyMSIgLz4KPC9nPgo8L3N2Zz4=');
|
||||
background-position: 8px center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 18px;
|
||||
}
|
||||
|
||||
#search::placeholder {
|
||||
color: #8e8e93;
|
||||
}
|
||||
|
||||
.dark #search {
|
||||
background-color: #1f2937;
|
||||
color: #ffffff;
|
||||
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZT0iI2JiYmJiYiI+CjxjaXJjbGUgY3g9IjEwLjUiIGN5PSIxMC41IiByPSI3LjUiIC8+CjxsaW5lIHgxPSIxNiIgeTE9IjE2IiB4Mj0iMjEiIHkyPSIyMSIgLz4KPC9nPgo8L3N2Zz4=');
|
||||
}
|
||||
|
||||
.dark #search::placeholder {
|
||||
color: #8e8e93;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
#search {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#top-pagination {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<section class="py-10 px-5 mt-[66px] dark:text-white">
|
||||
<div class="container">
|
||||
<h1 class="text-[38px] text-center font-bold text-active-blue mb-8">SimpleX Directory</h1>
|
||||
<p>Welcome to the selected users' communities that you can join via <a href="/downloads">SimpleX Chat
|
||||
app</a>.</p>
|
||||
<p>SimpleX Directory is also available as a <a href="https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok" target="_blank">SimpleX chat bot</a>.</p>
|
||||
<p>Read about <a href="/docs/directory.html">how to add</a> your community</a>.</p>
|
||||
<div class="search-container">
|
||||
<input id="search" autocomplete="off">
|
||||
<div id="top-pagination" class="pagination">
|
||||
<button class="text-btn live">Active</button>
|
||||
<button class="text-btn new">New</button>
|
||||
<button class="text-btn top">All</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="directory" style="height: 3000px;"></div>
|
||||
<div id="bottom-pagination" class="pagination"></div>
|
||||
</div>
|
||||
</section>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 308 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 326-->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 25.8008 25.459">
|
||||
<g>
|
||||
<rect height="25.459" opacity="0" width="25.8008" x="0" y="0" />
|
||||
<path
|
||||
d="M25.4395 12.7344C25.4395 19.7461 19.7266 25.459 12.7148 25.459C5.71289 25.459 0 19.7461 0 12.7344C0 5.73242 5.71289 0.0195312 12.7148 0.0195312C19.7266 0.0195312 25.4395 5.73242 25.4395 12.7344ZM4.21875 17.1094C4.21875 17.6855 4.53125 17.9785 5.27344 17.9785L10.1565 17.9785C10.0126 17.7153 9.96094 17.4251 9.96094 17.1582C9.96094 16.1237 10.5458 14.9671 11.5894 14.0584C10.8025 13.5615 9.83771 13.2715 8.84766 13.2715C6.47461 13.2715 4.21875 14.9512 4.21875 17.1094ZM10.8203 17.1582C10.8203 17.7148 11.1816 17.9785 12.0898 17.9785L20.1855 17.9785C21.1133 17.9785 21.4453 17.7148 21.4453 17.1582C21.4453 15.5371 19.4238 13.291 16.1621 13.291C12.8711 13.291 10.8203 15.5371 10.8203 17.1582ZM6.66016 9.87305C6.66016 11.2402 7.66602 12.2949 8.84766 12.2949C10.0488 12.2949 11.0449 11.2402 11.0449 9.86328C11.0449 8.53516 10.0293 7.5 8.84766 7.5C7.68555 7.5 6.66016 8.55469 6.66016 9.87305ZM13.6426 9.375C13.6426 10.9375 14.7754 12.1582 16.1621 12.1582C17.5195 12.1582 18.6621 10.9375 18.6621 9.36523C18.6621 7.83203 17.5098 6.65039 16.1621 6.65039C14.7852 6.65039 13.6426 7.86133 13.6426 9.375Z"
|
||||
fill="#ccc" fill-opacity="0.85" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
+52
-16
@@ -3,7 +3,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Simplex Chat</title>
|
||||
<link rel="stylesheet" href="/css/design3.css">
|
||||
</head>
|
||||
@@ -11,12 +11,17 @@
|
||||
<body>
|
||||
<div class="screen">
|
||||
<section class="page-1 cover">
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<h1>Be<br>Free</h1>
|
||||
<h2 class="gradient-text">Freedom & Security<br>of Your Communications</h2>
|
||||
<p>The first network where you own<br>your identity, contacts, and groups.</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h1>Be<br>Free</h1>
|
||||
<h2 class="gradient-text">Freedom & Security<br>of Your Communications</h2>
|
||||
<p>The first network where you own<br>your identity, contacts, and groups.</p>
|
||||
</div>
|
||||
<div class="socials">
|
||||
<a class="apple-store-btn hidden" href="https://apps.apple.com/us/app/simplex-chat/id1605771084" target="_blank"><img src="/img/new/apple_store.svg" /></a>
|
||||
<a class="google-play-btn hidden" href="https://play.google.com/store/apps/details?id=chat.simplex.app" target="_blank" title="Public iOS preview on TestFlight"><img src="/img/new/google_play.svg" /></a>
|
||||
<a class="f-droid-btn hidden" href="{{ '' if lang == 'en' else '/' ~ lang }}/fdroid" title="SimpleX F-Droid Repository"><img src="/img/new/f_droid.svg" /></a>
|
||||
<a class="testflight-btn hidden" href="https://testflight.apple.com/join/DWuT2LQu" target="_blank"><img class="no-border" src="/img/design_3/testflight-dark.png" /></a>
|
||||
<a class="android-btn hidden" href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk" target="_blank"><img src="/img/design_3/android-dark.png" /></a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -26,10 +31,10 @@
|
||||
<section class="page-2">
|
||||
<div class="image"></div>
|
||||
<div class="text-container">
|
||||
<h2>World's<br>Most Secure<br>Messaging</h2>
|
||||
<p>SimpleX messaging has<br>cutting-edge end-to-end<br>encryption.</p>
|
||||
<p>For your security, servers can’t<br>see your messages <span>or who you<br>talk to.</span></p>
|
||||
<a class="gradient-text" href="/learn-more">Learn more about<br>SimpleX messaging</a>
|
||||
<h2>World's Most Secure Messaging</h2>
|
||||
<p>SimpleX messaging has cutting-edge end-to-end encryption.</p>
|
||||
<p>For your security, servers can’t see your messages <span>or who you talk to.</span></p>
|
||||
<a class="gradient-text" href="/learn-more">Learn more about SimpleX messaging</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -37,24 +42,55 @@
|
||||
<div class="image"></div>
|
||||
<div class="text-container">
|
||||
<h2>You Own<br>The Next Web</h2>
|
||||
<p>SimpleX is founded on the belief that <span>you<br>must own your identity, contacts and<br>communities.</span></p>
|
||||
<p>Open and decentralized network lets you<br>connect with people and share ideas: be<br>free and secure.</p>
|
||||
<p>SimpleX is founded on the belief that <span>you must own your identity, contacts and communities.</span></p>
|
||||
<p>Open and decentralized network lets you connect with people and share ideas: be free and secure.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="page-4">
|
||||
<div class="image"></div>
|
||||
<div class="text-container">
|
||||
<h2>Communities<br>That Last</h2>
|
||||
<p>You will support your favorite groups with<br>future Community vouchers.</p>
|
||||
<p>Vouchers will pay for servers, to let your<br>communities stay free and independent.</p>
|
||||
<h2>Communities That Last</h2>
|
||||
<p>You will support your favorite groups with future Community vouchers.</p>
|
||||
<p>Vouchers will pay for servers, to let your communities stay free and independent.</p>
|
||||
<a class="gradient-text" href="/">Learn more about Community Vouchers</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="page-6">
|
||||
<div class="image"></div>
|
||||
<div class="text-container">
|
||||
<h2>SimpleX Roadmap to Free Internet</h2>
|
||||
<div class="roadmap">
|
||||
<p>2025</p>
|
||||
<p>Scale to Large Communities</p>
|
||||
<p>Escaping centralized platforms</p>
|
||||
</div>
|
||||
<div class="roadmap">
|
||||
<p>2026</p>
|
||||
<p>Sustainable Communities & Servers</p>
|
||||
<p>Launching Community Vouchers</p>
|
||||
</div>
|
||||
<div class="roadmap">
|
||||
<p>2027</p>
|
||||
<p>Make Your Communities Grow</p>
|
||||
<p>Tools to promote your communities</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/design3.js"></script>
|
||||
<script>
|
||||
(function setVH(){
|
||||
const apply = () =>
|
||||
document.documentElement.style.setProperty('--vh', (window.innerHeight * 0.01) + 'px');
|
||||
apply();
|
||||
window.addEventListener('resize', apply);
|
||||
window.addEventListener('orientationchange', apply);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,3 +1,32 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const isMobile = {
|
||||
Android: () => navigator.userAgent.match(/Android/i),
|
||||
iOS: () => navigator.userAgent.match(/iPhone|iPad|iPod/i)
|
||||
};
|
||||
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const googlePlayBtn = document.querySelector('.google-play-btn');
|
||||
const appleStoreBtn = document.querySelector('.apple-store-btn');
|
||||
const fDroidBtn = document.querySelector('.f-droid-btn');
|
||||
const testflightBtn = document.querySelector('.testflight-btn');
|
||||
const androidBtn = document.querySelector('.android-btn');
|
||||
|
||||
if (!googlePlayBtn || !appleStoreBtn || !fDroidBtn || !testflightBtn || !androidBtn) return;
|
||||
|
||||
|
||||
if (isMobile.Android()) {
|
||||
googlePlayBtn.classList.remove('hidden');
|
||||
fDroidBtn.classList.remove('hidden');
|
||||
androidBtn.classList.remove('hidden');
|
||||
}
|
||||
else if (isMobile.iOS()) {
|
||||
appleStoreBtn.classList.remove('hidden');
|
||||
testflightBtn.classList.remove('hidden');
|
||||
}
|
||||
else {
|
||||
appleStoreBtn.classList.remove('hidden');
|
||||
googlePlayBtn.classList.remove('hidden');
|
||||
fDroidBtn.classList.remove('hidden');
|
||||
testflightBtn.classList.remove('hidden');
|
||||
androidBtn.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,511 @@
|
||||
(function() {
|
||||
const directoryDataURL = 'https://directory.simplex.chat/data/';
|
||||
|
||||
// const directoryDataURL = 'http://localhost:8080/directory-data/';
|
||||
|
||||
let allEntries = [];
|
||||
|
||||
let filteredEntries = [];
|
||||
|
||||
let currentSortMode = '';
|
||||
|
||||
let currentSearch = '';
|
||||
|
||||
let currentPage = 1;
|
||||
|
||||
async function initDirectory() {
|
||||
const listing = await fetchJSON(directoryDataURL + 'listing.json')
|
||||
const liveBtn = document.querySelector('#top-pagination .live');
|
||||
const newBtn = document.querySelector('#top-pagination .new');
|
||||
const topBtn = document.querySelector('#top-pagination .top');
|
||||
const searchInput = document.getElementById('search');
|
||||
allEntries = listing.entries
|
||||
renderEntries('top', bySortPriority, topBtn)
|
||||
searchInput.addEventListener('input', (e) => renderEntries('top', bySortPriority, topBtn, e.target.value.trim()));
|
||||
liveBtn.addEventListener('click', () => renderEntries('live', byActiveAtDesc, liveBtn));
|
||||
newBtn.addEventListener('click', () => renderEntries('new', byCreatedAtDesc, newBtn));
|
||||
topBtn.addEventListener('click', () => renderEntries('top', bySortPriority, topBtn));
|
||||
|
||||
function renderEntries(mode, comparator, btn, search = '') {
|
||||
if (currentSortMode === mode && search == currentSearch) return;
|
||||
currentSortMode = mode;
|
||||
if (location.hash) location.hash = '';
|
||||
liveBtn.classList.remove('active');
|
||||
newBtn.classList.remove('active');
|
||||
topBtn.classList.remove('active');
|
||||
if (search == '') {
|
||||
currentSearch = '';
|
||||
currentPage = 1;
|
||||
searchInput.value = '';
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
currentSearch = search;
|
||||
}
|
||||
filteredEntries = filterEntries(mode, search ?? '').sort(comparator);
|
||||
renderDirectoryPage();
|
||||
}
|
||||
}
|
||||
|
||||
function renderDirectoryPage() {
|
||||
const currentEntries = addPagination(filteredEntries);
|
||||
displayEntries(currentEntries);
|
||||
}
|
||||
|
||||
function filterEntries(mode, s) {
|
||||
if (s === '' && mode == 'top') return allEntries.slice();
|
||||
const query = s.toLowerCase();
|
||||
return allEntries.filter(entry =>
|
||||
( mode === 'top'
|
||||
|| (mode === 'new' && entry.createdAt)
|
||||
|| (mode === 'live' && entry.activeAt)
|
||||
) &&
|
||||
( query === ''
|
||||
|| (entry.displayName || '').toLowerCase().includes(query)
|
||||
|| includesQuery(entry.shortDescr, query)
|
||||
|| includesQuery(entry.welcomeMessage, query)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function includesQuery(field, query) {
|
||||
return field
|
||||
&& Array.isArray(field)
|
||||
&& field.some(ft => {
|
||||
switch (ft.format?.type) {
|
||||
case 'uri': return uriIncludesQuery(ft.text, query);
|
||||
case 'hyperLink': return textIncludesQuery(ft.format.showText, query) || uriIncludesQuery(ft.format.linkUri, query);
|
||||
case 'simplexLink': return textIncludesQuery(ft.format.showText, query);
|
||||
default: return textIncludesQuery(ft.text, query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function textIncludesQuery(text, query) {
|
||||
return text ? text.toLowerCase().includes(query) : false
|
||||
}
|
||||
|
||||
function uriIncludesQuery(uri, query) {
|
||||
if (!uri) return false;
|
||||
uri = uri.toLowerCase();
|
||||
return !uri.includes('simplex') && uri.includes(query);
|
||||
}
|
||||
|
||||
async function fetchJSON(url) {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) throw new Error(`HTTP status: ${response.status}`)
|
||||
return await response.json()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function bySortPriority(entry1, entry2) {
|
||||
return entrySortPriority(entry2) - entrySortPriority(entry1);
|
||||
}
|
||||
|
||||
function byActiveAtDesc(entry1, entry2) {
|
||||
return (roundedTs(entry2.activeAt) - roundedTs(entry1.activeAt)) * 10
|
||||
+ Math.sign(bySortPriority(entry1, entry2));
|
||||
}
|
||||
|
||||
function byCreatedAtDesc(entry1, entry2) {
|
||||
return (roundedTs(entry2.createdAt) - roundedTs(entry1.createdAt)) * 10
|
||||
+ Math.sign(bySortPriority(entry1, entry2));
|
||||
}
|
||||
|
||||
function roundedTs(s) {
|
||||
try {
|
||||
return new Date(s).valueOf();
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const simplexUsersGroup = 'SimpleX users group';
|
||||
|
||||
function entrySortPriority(entry) {
|
||||
return entry.displayName === simplexUsersGroup
|
||||
? Number.MAX_VALUE
|
||||
: entryMemberCount(entry)
|
||||
}
|
||||
|
||||
function entryMemberCount(entry) {
|
||||
return entry.entryType.type == 'group'
|
||||
? (entry.entryType.summary?.currentMembers ?? 0)
|
||||
: 0
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const nowVal = now.valueOf();
|
||||
const today = new Date(now);
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const todayVal = today.valueOf();
|
||||
const todayYear = today.getFullYear();
|
||||
|
||||
const dateFormatter = Intl?.DateTimeFormat?.(undefined, {month: '2-digit', day: '2-digit'});
|
||||
const dateYearFormatter = Intl?.DateTimeFormat?.(undefined, {year: 'numeric', month: '2-digit', day: '2-digit'});
|
||||
|
||||
function showDate(d) {
|
||||
return dateFormatter && d.getFullYear() == todayYear
|
||||
? dateFormatter.format(d)
|
||||
: dateYearFormatter?.format(d) ?? d.toLocaleDateString();
|
||||
}
|
||||
|
||||
function showCreatedOn(s) {
|
||||
const d = new Date(s)
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return 'Created' + (d.valueOf() === todayVal ? ' today' : ' on ' + showDate(d));
|
||||
}
|
||||
|
||||
function showActiveOn(s) {
|
||||
const d = new Date(s)
|
||||
const ago = nowVal - d.valueOf();
|
||||
if (ago <= 1200000) return 'Active now'; // 20 minutes
|
||||
if (ago <= 10800000) return 'Active recently'; // 3 hours
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return 'Active' + (d.valueOf() === todayVal ? ' today' : ' on ' + showDate(d));
|
||||
}
|
||||
|
||||
function displayEntries(entries) {
|
||||
const directory = document.getElementById('directory');
|
||||
directory.innerHTML = '';
|
||||
|
||||
for (let entry of entries) {
|
||||
try {
|
||||
const { entryType, displayName, groupLink, shortDescr, welcomeMessage, imageFile } = entry;
|
||||
const entryDiv = document.createElement('div');
|
||||
entryDiv.className = 'entry w-full flex flex-col items-start md:flex-row rounded-[4px] overflow-hidden shadow-[0px_20px_30px_rgba(0,0,0,0.12)] dark:shadow-none bg-white dark:bg-[#11182F] mb-8';
|
||||
|
||||
const textContainer = document.createElement('div');
|
||||
textContainer.className = 'text-container';
|
||||
|
||||
const nameElement = document.createElement('h2');
|
||||
nameElement.textContent = displayName;
|
||||
nameElement.className = 'text-grey-black dark:text-white !text-lg md:!text-xl font-bold';
|
||||
textContainer.appendChild(nameElement);
|
||||
|
||||
const welcomeMessageHTML = welcomeMessage ? renderMarkdown(welcomeMessage) : undefined;
|
||||
const shortDescrHTML = shortDescr ? renderMarkdown(shortDescr) : undefined;
|
||||
if (shortDescrHTML && welcomeMessageHTML?.includes(shortDescrHTML) !== true) {
|
||||
const descrElement = document.createElement('p');
|
||||
descrElement.innerHTML = renderMarkdown(shortDescr);
|
||||
textContainer.appendChild(descrElement);
|
||||
}
|
||||
|
||||
if (welcomeMessageHTML) {
|
||||
const messageElement = document.createElement('p');
|
||||
messageElement.innerHTML = welcomeMessageHTML;
|
||||
textContainer.appendChild(messageElement);
|
||||
|
||||
const readMore = document.createElement('p');
|
||||
readMore.textContent = 'Read more';
|
||||
readMore.className = 'read-more';
|
||||
readMore.style.display = 'none';
|
||||
textContainer.appendChild(readMore);
|
||||
|
||||
setTimeout(() => {
|
||||
const computedStyle = window.getComputedStyle(messageElement);
|
||||
const lineHeight = parseFloat(computedStyle.lineHeight);
|
||||
const maxLines = 5;
|
||||
const maxHeight = maxLines * lineHeight
|
||||
const maxHeightPx = `${maxHeight}px`;
|
||||
messageElement.style.maxHeight = maxHeightPx;
|
||||
messageElement.style.overflow = 'hidden';
|
||||
|
||||
if (messageElement.scrollHeight > maxHeight + 4) {
|
||||
readMore.style.display = 'block';
|
||||
readMore.addEventListener('click', () => {
|
||||
if (messageElement.style.maxHeight === maxHeightPx) {
|
||||
messageElement.style.maxHeight = 'none';
|
||||
readMore.className = 'read-less';
|
||||
readMore.innerHTML = '▲';
|
||||
} else {
|
||||
messageElement.style.maxHeight = maxHeightPx;
|
||||
readMore.className = 'read-more';
|
||||
readMore.textContent = 'Read more';
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
const entryTimestamp = currentSortMode === 'new' && entry.createdAt
|
||||
? showCreatedOn(entry.createdAt)
|
||||
: entry.activeAt
|
||||
? showActiveOn(entry.activeAt)
|
||||
: '';
|
||||
if (entryTimestamp) {
|
||||
timestampElement = document.createElement('p');
|
||||
timestampElement.textContent = entryTimestamp;
|
||||
timestampElement.className = 'text-sm';
|
||||
textContainer.appendChild(timestampElement);
|
||||
}
|
||||
|
||||
const memberCount = entryMemberCount(entry);
|
||||
if (typeof memberCount == 'number' && memberCount > 0) {
|
||||
const memberCountElement = document.createElement('p');
|
||||
memberCountElement.textContent = `${memberCount} members`;
|
||||
memberCountElement.className = 'text-sm';
|
||||
textContainer.appendChild(memberCountElement);
|
||||
}
|
||||
|
||||
const imgLinkElement = document.createElement('a');
|
||||
const groupLinkUri = groupLink.connShortLink ?? groupLink.connFullLink
|
||||
try {
|
||||
imgLinkElement.href = platformSimplexUri(groupLinkUri);
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
imgLinkElement.href = groupLinkUri;
|
||||
}
|
||||
imgLinkElement.target = "_blank";
|
||||
imgLinkElement.title = `Join ${displayName}`;
|
||||
|
||||
const imgElement = document.createElement('img');
|
||||
imgElement.src = imageFile ? directoryDataURL + imageFile : '/img/group.svg';
|
||||
imgElement.alt = displayName;
|
||||
imgElement.addEventListener('error', () => imgElement.src = '/img/group.svg');
|
||||
imgLinkElement.appendChild(imgElement);
|
||||
entryDiv.appendChild(imgLinkElement);
|
||||
|
||||
entryDiv.appendChild(textContainer);
|
||||
directory.appendChild(entryDiv);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
for (let el of document.querySelectorAll('.secret')) {
|
||||
el.addEventListener('click', () => el.classList.toggle('visible'));
|
||||
}
|
||||
|
||||
directory.style.height = '';
|
||||
}
|
||||
|
||||
function goToPage(p) {
|
||||
currentPage = p;
|
||||
renderDirectoryPage();
|
||||
}
|
||||
|
||||
function addPagination(entries) {
|
||||
const entriesPerPage = 10;
|
||||
const totalPages = Math.ceil(entries.length / entriesPerPage);
|
||||
if (currentPage < 1) currentPage = 1;
|
||||
if (currentPage > totalPages) currentPage = totalPages;
|
||||
|
||||
const startIndex = (currentPage - 1) * entriesPerPage;
|
||||
const endIndex = Math.min(startIndex + entriesPerPage, entries.length);
|
||||
const currentEntries = entries.slice(startIndex, endIndex);
|
||||
|
||||
// addPaginationElements('top-pagination')
|
||||
addPaginationElements('bottom-pagination')
|
||||
return currentEntries;
|
||||
|
||||
function addPaginationElements(paginationId) {
|
||||
const pagination = document.getElementById(paginationId);
|
||||
if (!pagination) {
|
||||
return currentEntries;
|
||||
}
|
||||
pagination.innerHTML = '';
|
||||
|
||||
try {
|
||||
let startPage, endPage;
|
||||
const pageButtonCount = 8
|
||||
if (totalPages <= pageButtonCount) {
|
||||
startPage = 1;
|
||||
endPage = totalPages;
|
||||
} else {
|
||||
startPage = Math.max(1, currentPage - 4);
|
||||
endPage = Math.min(totalPages, startPage + pageButtonCount - 1);
|
||||
if (endPage - startPage + 1 < pageButtonCount) {
|
||||
startPage = Math.max(1, endPage - pageButtonCount + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// if (currentPage > 1 && startPage > 1) {
|
||||
// const firstBtn = document.createElement('button');
|
||||
// firstBtn.textContent = 'First';
|
||||
// firstBtn.classList.add('text-btn');
|
||||
// firstBtn.addEventListener('click', () => goToPage(1));
|
||||
// pagination.appendChild(firstBtn);
|
||||
// }
|
||||
|
||||
if (currentPage > 1) {
|
||||
const prevBtn = document.createElement('button');
|
||||
prevBtn.textContent = 'Prev';
|
||||
prevBtn.classList.add('text-btn');
|
||||
prevBtn.addEventListener('click', () => goToPage(currentPage - 1));
|
||||
pagination.appendChild(prevBtn);
|
||||
}
|
||||
|
||||
for (let p = startPage; p <= endPage; p++) {
|
||||
const pageBtn = document.createElement('button');
|
||||
pageBtn.textContent = p.toString();
|
||||
if (p === currentPage) {
|
||||
pageBtn.classList.add('active');
|
||||
} else if (p === currentPage - 1 || p === currentPage + 1) {
|
||||
pageBtn.classList.add('neighbor');
|
||||
}
|
||||
pageBtn.addEventListener('click', () => goToPage(p));
|
||||
pagination.appendChild(pageBtn);
|
||||
}
|
||||
|
||||
if (currentPage < totalPages) {
|
||||
const nextBtn = document.createElement('button');
|
||||
nextBtn.textContent = 'Next';
|
||||
nextBtn.classList.add('text-btn');
|
||||
nextBtn.addEventListener('click', () => goToPage(currentPage + 1));
|
||||
pagination.appendChild(nextBtn);
|
||||
}
|
||||
|
||||
// if (endPage < totalPages) {
|
||||
// const lastBtn = document.createElement('button');
|
||||
// lastBtn.textContent = 'Last';
|
||||
// lastBtn.classList.add('text-btn');
|
||||
// lastBtn.addEventListener('click', () => goToPage(totalPages));
|
||||
// pagination.appendChild(lastBtn);
|
||||
// }
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initDirectory);
|
||||
} else {
|
||||
initDirectory();
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
.replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
function getSimplexLinkDescr(linkType) {
|
||||
switch (linkType) {
|
||||
case 'contact': return 'SimpleX contact address';
|
||||
case 'invitation': return 'SimpleX one-time invitation';
|
||||
case 'group': return 'SimpleX group link';
|
||||
case 'channel': return 'SimpleX channel link';
|
||||
case 'relay': return 'SimpleX relay link';
|
||||
default: return 'SimpleX link';
|
||||
}
|
||||
}
|
||||
|
||||
function viaHost(smpHosts) {
|
||||
const first = smpHosts[0] ?? '?';
|
||||
return `via ${first}`;
|
||||
}
|
||||
|
||||
function isCurrentSite(uri) {
|
||||
return uri.startsWith("https://simplex.chat") || uri.startsWith("https://www.simplex.chat")
|
||||
}
|
||||
|
||||
function targetBlank(uri) {
|
||||
return isCurrentSite(uri) ? '' : ' target="_blank"'
|
||||
}
|
||||
|
||||
const simplexAddressRegexp = /^simplex:\/([a-z]+)#(.+)/i;
|
||||
|
||||
const simplexShortLinkTypes = ["a", "c", "g", "i", "r"];
|
||||
|
||||
function platformSimplexUri(uri) {
|
||||
if (isMobile.any()) return uri;
|
||||
const res = uri.match(simplexAddressRegexp);
|
||||
if (!res || !Array.isArray(res) || res.length < 3) return uri;
|
||||
const linkType = res[1];
|
||||
const fragment = res[2];
|
||||
if (simplexShortLinkTypes.includes(linkType)) {
|
||||
const queryIndex = fragment.indexOf('?');
|
||||
if (queryIndex === -1) return uri;
|
||||
const hashPart = fragment.substring(0, queryIndex);
|
||||
const queryStr = fragment.substring(queryIndex + 1);
|
||||
const params = new URLSearchParams(queryStr);
|
||||
const host = params.get('h');
|
||||
if (!host) return uri;
|
||||
params.delete('h');
|
||||
let newFragment = hashPart;
|
||||
const remainingParams = params.toString();
|
||||
if (remainingParams) newFragment += '?' + remainingParams;
|
||||
return `https://${host}:/${linkType}#${newFragment}`;
|
||||
} else {
|
||||
return `https://simplex.chat/${linkType}#${fragment}`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderMarkdown(fts) {
|
||||
let html = '';
|
||||
for (const ft of fts) {
|
||||
const { format, text } = ft;
|
||||
if (!format) {
|
||||
html += escapeHtml(text);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
switch (format.type) {
|
||||
case 'bold':
|
||||
html += `<strong>${escapeHtml(text)}</strong>`;
|
||||
break;
|
||||
case 'italic':
|
||||
html += `<em>${escapeHtml(text)}</em>`;
|
||||
break;
|
||||
case 'strikeThrough':
|
||||
html += `<s>${escapeHtml(text)}</s>`;
|
||||
break;
|
||||
case 'snippet':
|
||||
html += `<span style="font-family: monospace;">${escapeHtml(text)}</span>`;
|
||||
break;
|
||||
case 'secret':
|
||||
html += `<span class="secret">${escapeHtml(text)}</span>`;
|
||||
break;
|
||||
case 'colored':
|
||||
html += `<span class="${format.color}">${escapeHtml(text)}</span>`;
|
||||
break;
|
||||
case 'uri':
|
||||
let href = text.startsWith('http://') || text.startsWith('https://') || text.startsWith('simplex:/') ? text : 'https://' + text;
|
||||
html += `<a href="${href}"${targetBlank(href)}>${escapeHtml(text)}</a>`;
|
||||
break;
|
||||
case 'hyperLink': {
|
||||
const { showText, linkUri } = format;
|
||||
html += `<a href="${linkUri}"${targetBlank(linkUri)}>${escapeHtml(showText ?? linkUri)}</a>`;
|
||||
break;
|
||||
}
|
||||
case 'simplexLink': {
|
||||
const { showText, linkType, simplexUri, smpHosts } = format;
|
||||
const linkText = showText ? escapeHtml(showText) : getSimplexLinkDescr(linkType);
|
||||
html += `<a href="${platformSimplexUri(simplexUri)}" target="_blank">${linkText} <em>(${viaHost(smpHosts)})</em></a>`;
|
||||
break;
|
||||
}
|
||||
case 'command':
|
||||
html += `<span style="font-family: monospace;">${escapeHtml(text)}</span>`;
|
||||
break;
|
||||
case 'mention':
|
||||
html += `<strong>${escapeHtml(text)}</strong>`;
|
||||
break;
|
||||
case 'email':
|
||||
html += `<a href="mailto:${text}">${escapeHtml(text)}</a>`;
|
||||
break;
|
||||
case 'phone':
|
||||
html += `<a href="tel:${text}">${escapeHtml(text)}</a>`;
|
||||
break;
|
||||
case 'unknown':
|
||||
html += escapeHtml(text);
|
||||
break;
|
||||
default:
|
||||
html += escapeHtml(text);
|
||||
}
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
html += escapeHtml(text);
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
})();
|
||||
@@ -26,7 +26,8 @@ const uniqueSwiper = new Swiper('.unique-swiper', {
|
||||
|
||||
const isMobile = {
|
||||
Android: () => navigator.userAgent.match(/Android/i),
|
||||
iOS: () => navigator.userAgent.match(/iPhone|iPad|iPod/i)
|
||||
iOS: () => navigator.userAgent.match(/iPhone|iPad|iPod/i),
|
||||
any: () => navigator.userAgent.match(/Android|iPhone|iPad|iPod/i)
|
||||
};
|
||||
|
||||
const privateSwiper = new Swiper('.private-swiper', {
|
||||
|
||||
Reference in New Issue
Block a user