diff --git a/app/mobile/ios/Databag.xcodeproj/project.pbxproj b/app/mobile/ios/Databag.xcodeproj/project.pbxproj index 4400d142..c36d2958 100644 --- a/app/mobile/ios/Databag.xcodeproj/project.pbxproj +++ b/app/mobile/ios/Databag.xcodeproj/project.pbxproj @@ -571,7 +571,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_CXX_LANGUAGE_STANDARD = "c++14"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -643,7 +643,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_CXX_LANGUAGE_STANDARD = "c++14"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; diff --git a/app/mobile/package.json b/app/mobile/package.json index 4f2d52a1..f10a5886 100644 --- a/app/mobile/package.json +++ b/app/mobile/package.json @@ -10,6 +10,7 @@ "test": "jest" }, "dependencies": { + "@braintree/sanitize-url": "^6.0.2", "@react-native-clipboard/clipboard": "^1.11.1", "@react-native-firebase/app": "^17.2.0", "@react-native-firebase/messaging": "^17.2.0", diff --git a/app/mobile/src/access/admin/Admin.jsx b/app/mobile/src/access/admin/Admin.jsx index 9e5fc68f..09fef349 100644 --- a/app/mobile/src/access/admin/Admin.jsx +++ b/app/mobile/src/access/admin/Admin.jsx @@ -56,22 +56,24 @@ export function Admin() { - - - View Terms of Service - - actions.agree(!state.agree)}> - { state.agree && ( - - )} - { !state.agree && ( - - )} - I agree to Terms of Service - - + { Platform.OS !== 'ios' && ( + + + View Terms of Service + + actions.agree(!state.agree)}> + { state.agree && ( + + )} + { !state.agree && ( + + )} + I agree to Terms of Service + + + )} - { state.enabled && state.agree && ( + { state.enabled && (Platform.OS === 'ios' || state.agree) && ( { state.busy && ( @@ -81,7 +83,7 @@ export function Admin() { )} )} - { (!state.enabled || !state.agree) && ( + { (!state.enabled || (Platform.OS !== 'ios' && !state.agree)) && ( Access diff --git a/app/mobile/src/access/create/Create.jsx b/app/mobile/src/access/create/Create.jsx index 43e426ff..afe5ff1e 100644 --- a/app/mobile/src/access/create/Create.jsx +++ b/app/mobile/src/access/create/Create.jsx @@ -138,23 +138,25 @@ export function Create() { )} - - - View Terms of Service - - actions.agree(!state.agree)}> - { state.agree && ( - - )} - { !state.agree && ( - - )} - I agree to Terms of Service - - + { Platform.OS !== 'ios' && ( + + + View Terms of Service + + actions.agree(!state.agree)}> + { state.agree && ( + + )} + { !state.agree && ( + + )} + I agree to Terms of Service + + + )} - { state.enabled && state.agree && ( + { state.enabled && (Platform.OS === 'ios' || state.agree) && ( { state.busy && ( @@ -164,7 +166,7 @@ export function Create() { )} )} - { (!state.enabled || !state.agree) && ( + { (!state.enabled || (Platform.OS !== 'ios' && !state.agree)) && ( Create Account diff --git a/app/mobile/src/access/login/Login.jsx b/app/mobile/src/access/login/Login.jsx index cc1f6028..78793513 100644 --- a/app/mobile/src/access/login/Login.jsx +++ b/app/mobile/src/access/login/Login.jsx @@ -1,4 +1,4 @@ -import { KeyboardAvoidingView, ActivityIndicator, Modal, ScrollView, Alert, Text, TextInput, View, TouchableOpacity } from 'react-native'; +import { Platform, KeyboardAvoidingView, ActivityIndicator, Modal, ScrollView, Alert, Text, TextInput, View, TouchableOpacity } from 'react-native'; import { styles } from './Login.styled'; import Ionicons from 'react-native-vector-icons/AntDesign'; import { useLogin } from './useLogin.hook'; @@ -63,22 +63,24 @@ export function Login() { )} - - - View Terms of Service - - actions.agree(!state.agree)}> - { state.agree && ( - - )} - { !state.agree && ( - - )} - I agree to Terms of Service - - + { Platform.OS !== 'ios' && ( + + + View Terms of Service + + actions.agree(!state.agree)}> + { state.agree && ( + + )} + { !state.agree && ( + + )} + I agree to Terms of Service + + + )} - { state.enabled && state.agree && ( + { state.enabled && (Platform.OS === 'ios' || state.agree) && ( { state.busy && ( @@ -88,7 +90,7 @@ export function Login() { )} )} - { (!state.enabled || !state.agree) && ( + { (!state.enabled || (Platform.OS !== 'ios' && !state.agree)) && ( Login diff --git a/app/mobile/src/access/reset/Reset.jsx b/app/mobile/src/access/reset/Reset.jsx index 14f01516..26d029a8 100644 --- a/app/mobile/src/access/reset/Reset.jsx +++ b/app/mobile/src/access/reset/Reset.jsx @@ -47,23 +47,25 @@ export function Reset() { autoCapitalize="none" placeholder="token" placeholderTextColor={Colors.grey} /> + + { Platform.OS !== 'ios' && ( + + + View Terms of Service + + actions.agree(!state.agree)}> + { state.agree && ( + + )} + { !state.agree && ( + + )} + I agree to Terms of Service + + + )} - - - View Terms of Service - - actions.agree(!state.agree)}> - { state.agree && ( - - )} - { !state.agree && ( - - )} - I agree to Terms of Service - - - - { state.enabled && state.agree && ( + { state.enabled && (Platform.OS === 'ios' || state.agree) && ( { state.busy && ( @@ -73,7 +75,7 @@ export function Reset() { )} )} - { (!state.enabled || !state.agree) && ( + { (!state.enabled || (Platform.OS !== 'ios' && !state.agree)) && ( Access diff --git a/app/mobile/src/session/conversation/topicItem/TopicItem.jsx b/app/mobile/src/session/conversation/topicItem/TopicItem.jsx index eed90eb5..46bccc1c 100644 --- a/app/mobile/src/session/conversation/topicItem/TopicItem.jsx +++ b/app/mobile/src/session/conversation/topicItem/TopicItem.jsx @@ -131,8 +131,8 @@ export function TopicItem({ item, focused, focus, hosting, remove, update, block return ( - - + + { state.logo !== 'avatar' && state.logo && ( )} @@ -141,7 +141,7 @@ export function TopicItem({ item, focused, focus, hosting, remove, update, block )} { state.name } { state.timestamp } - + { state.status === 'confirmed' && ( <> { state.transform === 'complete' && state.assets && ( @@ -162,8 +162,8 @@ export function TopicItem({ item, focused, focus, hosting, remove, update, block )} - { state.message && !state.sealed && ( - { state.message } + { state.clickable && !state.sealed && ( + { state.clickable } )} { state.sealed && ( sealed message @@ -175,7 +175,7 @@ export function TopicItem({ item, focused, focus, hosting, remove, update, block )} - + { focused && ( { state.editable && ( diff --git a/app/mobile/src/session/conversation/topicItem/useTopicItem.hook.js b/app/mobile/src/session/conversation/topicItem/useTopicItem.hook.js index f2330a4c..e47e0b9b 100644 --- a/app/mobile/src/session/conversation/topicItem/useTopicItem.hook.js +++ b/app/mobile/src/session/conversation/topicItem/useTopicItem.hook.js @@ -1,13 +1,15 @@ import { useState, useEffect, useContext } from 'react'; +import { Linking } from 'react-native'; import { ConversationContext } from 'context/ConversationContext'; import { CardContext } from 'context/CardContext'; import { ProfileContext } from 'context/ProfileContext'; import { AccountContext } from 'context/AccountContext'; import moment from 'moment'; -import { useWindowDimensions } from 'react-native'; +import { useWindowDimensions, Text } from 'react-native'; import Colors from 'constants/Colors'; import { getCardByGuid } from 'context/cardUtil'; import { decryptTopicSubject } from 'context/sealUtil'; +import { sanitizeUrl } from '@braintree/sanitize-url'; export function useTopicItem(item, hosting, remove, contentKey) { @@ -18,6 +20,7 @@ export function useTopicItem(item, hosting, remove, contentKey) { logo: null, timestamp: null, message: null, + clickable: null, carousel: false, carouselIndex: 0, width: null, @@ -90,12 +93,13 @@ export function useTopicItem(item, hosting, remove, contentKey) { } } - let parsed, sealed, message, assets, fontSize, fontColor; + let parsed, sealed, message, clickable, assets, fontSize, fontColor; if (dataType === 'superbasictopic') { try { sealed = false; parsed = JSON.parse(data); - message = parsed.text; + message = parsed?.text; + clickable = clickableText(parsed.text); assets = parsed.assets; if (parsed.textSize === 'small') { fontSize = 10; @@ -139,6 +143,7 @@ export function useTopicItem(item, hosting, remove, contentKey) { sealed = false; parsed = unsealed.message; message = parsed?.text; + clickable = clickableText(parsed?.text); if (parsed?.textSize === 'small') { fontSize = 10; } @@ -177,7 +182,7 @@ export function useTopicItem(item, hosting, remove, contentKey) { const editable = guid === identity?.guid && parsed; const deletable = editable || hosting; - updateState({ logo, name, nameSet, known, sealed, message, fontSize, fontColor, timestamp, transform, status, assets, deletable, editable, editData: parsed, editMessage: message, editType: dataType }); + updateState({ logo, name, nameSet, known, sealed, message, clickable, fontSize, fontColor, timestamp, transform, status, assets, deletable, editable, editData: parsed, editMessage: message, editType: dataType }); }, [conversation.state, card.state, account.state, item, contentKey]); const unsealTopic = async (topicId, revision, topicDetail) => { @@ -194,6 +199,31 @@ export function useTopicItem(item, hosting, remove, contentKey) { } }; + const clickableText = (text) => { + var pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path + '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string + '(\\#[-a-z\\d_]*)?$','i'); // fragment locator + + let clickable = []; + let group = ''; + const words = text == null ? [''] : text.split(' '); + words.forEach((word, index) => { + if (!!pattern.test(word)) { + clickable.push({ group }); + group = ''; + clickable.push( Linking.openURL(sanitizeUrl(word))} style={{ fontStyle: 'italic' }}>{ sanitizeUrl(word) + ' ' }); + } + else { + group += `${word} `; + } + }) + clickable.push({ group }); + return { clickable }; + }; + const actions = { showCarousel: (index) => { updateState({ carousel: true, carouselIndex: index }); diff --git a/app/mobile/yarn.lock b/app/mobile/yarn.lock index dca4a4cb..7c1a5cf7 100644 --- a/app/mobile/yarn.lock +++ b/app/mobile/yarn.lock @@ -1077,6 +1077,11 @@ resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@braintree/sanitize-url@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f" + integrity sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg== + "@egjs/hammerjs@^2.0.17": version "2.0.17" resolved "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz" diff --git a/net/web/package.json b/net/web/package.json index ec5cfc28..c493c3b2 100644 --- a/net/web/package.json +++ b/net/web/package.json @@ -22,6 +22,7 @@ "axios": "^0.27.2", "base-64": "^1.0.0", "crypto-js": "^4.1.1", + "dompurify": "^3.0.1", "jsencrypt": "^2.3.1", "react": "^18.2.0", "react-color": "^2.19.3", diff --git a/net/web/src/session/conversation/topicItem/TopicItem.jsx b/net/web/src/session/conversation/topicItem/TopicItem.jsx index df85f839..0136eb9b 100644 --- a/net/web/src/session/conversation/topicItem/TopicItem.jsx +++ b/net/web/src/session/conversation/topicItem/TopicItem.jsx @@ -123,7 +123,7 @@ export function TopicItem({ host, sealed, topic, update, remove }) { )} { !sealed && !state.editing && (
-
{ topic.text }
+
{ topic.clickable }
)} { state.editing && ( diff --git a/net/web/src/session/conversation/useConversation.hook.js b/net/web/src/session/conversation/useConversation.hook.js index 02c16da5..ff1608b4 100644 --- a/net/web/src/session/conversation/useConversation.hook.js +++ b/net/web/src/session/conversation/useConversation.hook.js @@ -9,6 +9,7 @@ import { ProfileContext } from 'context/ProfileContext'; import { isUnsealed, getChannelSeals, getContentKey, encryptTopicSubject } from 'context/sealUtil'; import { decryptTopicSubject } from 'context/sealUtil'; import { getProfileByGuid } from 'context/cardUtil'; +import * as DOMPurify from 'dompurify'; export function useConversation(cardId, channelId) { @@ -132,6 +133,31 @@ export function useConversation(cardId, channelId) { // eslint-disable-next-line }, [state.contentKey]); + const clickableText = (text) => { + var pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path + '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string + '(\\#[-a-z\\d_]*)?$','i'); // fragment locator + + let group = ''; + let clickable = []; + const words = text == null ? '' : DOMPurify.sanitize(text).split(' '); + words.forEach((word, index) => { + if (!!pattern.test(word)) { + clickable.push({ group }); + group = ''; + clickable.push({ `${word} ` }); + } + else { + group += `${word} `; + } + }) + clickable.push({ group }); + return

{ clickable }

; + }; + const syncTopic = (item, value) => { const revision = value.data?.detailRevision; const detail = value.data?.topicDetail || {}; @@ -190,6 +216,7 @@ export function useConversation(cardId, channelId) { const message = JSON.parse(detail.data); item.assets = message.assets; item.text = message.text; + item.clickable = clickableText(message.text); item.textColor = message.textColor ? message.textColor : '#444444'; item.textSize = message.textSize ? message.textSize : 14; } @@ -197,6 +224,7 @@ export function useConversation(cardId, channelId) { const subject = decryptTopicSubject(detail.data, state.contentKey); item.assets = subject.message.assets; item.text = subject.message.text; + item.clickable = clickableText(subject.message.text); item.textColor = subject.message.textColor ? subject.message.textColor : '#444444'; item.textSize = subject.message.textSize ? subject.message.textSize : 14; } diff --git a/net/web/yarn.lock b/net/web/yarn.lock index 6f57a770..76474821 100644 --- a/net/web/yarn.lock +++ b/net/web/yarn.lock @@ -4366,6 +4366,11 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" +dompurify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.1.tgz#a0933f38931b3238934dd632043b727e53004289" + integrity sha512-60tsgvPKwItxZZdfLmamp0MTcecCta3avOhsLgPZ0qcWt96OasFfhkeIRbJ6br5i0fQawT1/RBGB5L58/Jpwuw== + domutils@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz"