diff --git a/CORDOVA.md b/CORDOVA.md new file mode 100644 index 000000000..a96f8e023 --- /dev/null +++ b/CORDOVA.md @@ -0,0 +1,90 @@ +# Мобильное приложение GOLOS Блоги + +Работает на Android. + +```js +git clone https://github.com/golos-blockchain/ui-blogs +``` + +### Сборка приложения + +Для сборки требуется **Linux** (например, Ubuntu 18 - 20). + +1. Если у вас Linux современных версий (например **Ubuntu 20** или выше) - установите **Node.js 18** ([Windows](https://nodejs.org/dist/v18.20.5/node-v18.20.5-x64.msi), [Linux](https://github.com/nodesource/distributions/blob/master/README.md)). + Если у вас **Ubuntu 18** или иной старый Linux - установите **[Node.js 16](https://github.com/nodesource/distributions/blob/master/OLD_README.md#using-ubuntu-3)**. + +2. Установите Android Studio. + +3. Запустите Android Studio. Установите все, что будет предложено при установке. + +4. Создайте пустое приложение Android (с любым Activity и конфигурацией), соберите его. Это нужно для проверки правильности установки Android Studio. + +5. Запустите терминал. + +6. Установите глобальные зависимости: +```sh +npx yarn global add cordova@11.0.0 +``` + +7. Скачайте репозиторий с помощью git clone (команда есть выше), и зайдите в его папку с помощью: +```sh +cd ui-blogs +``` + +8. Внесите все **настройки** в файле `config/mobile.json`: + +- site_domain (пример: golos.id то есть основной домен Блогов) +- images +- auth_service +- notify_service +- wallet_service +- messenger_service +- app_updater + +9. Установите все зависимости (для сборки). + +```sh +npx yarn install +``` + +10. Выполните команду (/root/ - это должен быть ваш путь к папке профиля, в ней лежит папка Android): +```sh +export ANDROID_SDK_ROOT=/root/Android/Sdk +``` + +11. Установка совместимой системы сборки. + +Установите JDK (отдельно от Android Studio): +```sh +sudo apt-get install openjdk-8-jdk +``` +После этого команда `javac -version` должна выдавать ответ вида: javac 1.8.x + +Установите Gradle (отдельно от Android Studio): +```sh +sudo apt-get install gradle +``` + +Добавьте /root/Android/Sdk/platform-tools в переменную PATH (для adb, вспомогательное, для сборки не обязательно) + +12. Откройте Android Studio, а из нее откройте SDK Manager (кнопка "☰" -> Tools -> SDK Manager). Установите Android build tools 30.0.3. + Для этого слева выбираете **Languages & Frameworks -> Android SDK**, во вкладках сверх выбираете **SDK Tools**, включаете **Show package details**, отключаете **Hide obsolete packages**, после чего выбираете нужную версию Build tools и нажимаете Apply для установки. + +13. Если собираетесь автоматически устанавливать и запускать приложение (а не вручную, перекинув apk на устройство), то сделайте следующее. Подключите устройство по USB (разрешив отладку) и убедитесь, что adb видит его, выполнив в командной строке команду +```sh +adb devices +``` + +14. Соберите приложение. + +```sh +npx yarn run build:mobile +``` +Сборка происходит в два этапа - сначала собирается код React с помощью Webpack, затем на его основе собирается мобильное приложение с помощью Cordova. + +Этап Cordova может выдавать ошибки, которые нужно устранить дополнительно. + +В случае ошибки с требованием установить Build Tools нужной версии или иные компоненты, необходимо поступить как описано в пункте 12, выбрав нужную версию нужного компонента и установив. Затем снова попробовать собрать (используя при этом команду `npx yarn run postbuild:mobile`, чтобы не повторять этап Webpack, который уже выполнен). + +В случае успешной сборки собранный APK будет в `cordova/platforms/android/app/build/outputs/apk/debug` +Кроме того, APK сразу же устанавливается на устройство. Об успешной установке свидетельствует сообщение `INSTALL SUCCESS`. Ошибка типа `"sh" ?? ?????` говорит о том, что устройство не обнаружено (например, проблемы с разъемом, кабелем). diff --git a/app/MainApp.js b/app/MainApp.js index c541122a4..5f4141d33 100644 --- a/app/MainApp.js +++ b/app/MainApp.js @@ -2,12 +2,41 @@ import renderApp from 'app/renderApp' import golos from 'golos-lib-js' import tt from 'counterpart' +import { openAppSettings } from 'app/components/pages/app/AppSettings' import * as api from 'app/utils/APIWrapper' import getState from 'app/utils/StateBuilder' -import { checkUpdates } from './appUpdater' +import { addShortcut } from 'app/utils/app/ShortcutUtils' +import { checkUpdates } from 'app/utils/app/UpdateUtils' +import defaultCfg from 'app/app_cfg' require('app/cookieHelper') -const appConfig = window.appSettings.load() +let appConfig +if (process.env.MOBILE_APP) { + console.log('Loading app config...') + let cfg = localStorage.getItem('app_settings') + if (cfg) { + try { + cfg = JSON.parse(cfg) + // Add here migrations + cfg = { ...defaultCfg, ...cfg } + } catch (err) { + console.error('Cannot parse app_settings', err) + cfg = defaultCfg + } + } else { + cfg = defaultCfg + } + if (!cfg.ws_connection_client) { + cfg.ws_connection_client = cfg.ws_connection_app[0].address + } + if (cfg.images.use_img_proxy === undefined) { + cfg.images.use_img_proxy = true + } + cfg.app_version = defaultCfg.app_version + appConfig = cfg +} else { + appConfig = window.appSettings.load() +} const initialState = { offchain: { @@ -15,6 +44,7 @@ const initialState = { ...appConfig, blocked_users: [], blocked_posts: [], + filter_apps: [], add_notify_site: {} }, flash: { @@ -27,60 +57,106 @@ window.$STM_Config = initialState.offchain.config window.$STM_csrf = null // not used in app function closeSplash() { - if (window.appSplash) - window.appSplash.contentLoaded() + try { + if (process.env.MOBILE_APP) { + navigator.splashscreen.hide() + } else { + window.appSplash.contentLoaded() + } + } catch (err) { + console.error('closeSplash', err) + } +} + +const isSettings = () => { + return window.location.hash === '#app-settings' || + window.location.pathname === '/__app_settings' } function showNodeError() { - alert(tt('app_settings.node_error_NODE', { NODE: $STM_Config.ws_connection_client })) + if (isSettings()) return + if (confirm(tt('app_settings.node_error_new_NODE', { NODE: $STM_Config.ws_connection_client }) + + ' ' + tt('app_settings.node_error_new_NODE2') + '?')) { + openAppSettings() + } +} + +const showError = (err, label = '') => { + if (!process.env.MOBILE_APP) return + alert(label + ' error:\n' + + (err && err.toString()) + '\n' + + (err && JSON.stringify(err.stack)) + ) } async function initState() { - // these are need for getState - await golos.importNativeLib(); - const config = initialState.offchain.config - golos.config.set('websocket', config.ws_connection_client) - if (config.chain_id) - golos.config.set('chain_id', config.chain_id) - - const { pathname } = window.location - if (pathname.startsWith('/__app_')) { - return { - content: {} + try { + // these are need for getState + await golos.importNativeLib(); + const config = initialState.offchain.config + golos.config.set('websocket', config.ws_connection_client) + if (config.chain_id) + golos.config.set('chain_id', config.chain_id) + + const { pathname } = window.location + if (pathname.startsWith('/__app_')) { + return { + content: {} + } } - } - let splashTimeout = setTimeout(() => { - closeSplash() - showNodeError() - }, 30000) + // First add - for case if all failed at all, and not rendering + if (process.env.MOBILE_APP) { + await addShortcut({ + id: 'the_settings', + shortLabel: 'Настройки', + longLabel: 'Настройки', + hash: '#app-settings' + }) + } + + let splashTimeout = setTimeout(() => { + closeSplash() + showNodeError() + }, 30000) + + const doUpdate = Math.random() > 0.7 + if (doUpdate) { + try { + $STM_Config.add_notify_site = await checkUpdates() + } catch (err) { + console.error('Cannot check updates', err) + clearTimeout(splashTimeout) + closeSplash() + alert('Cannot check updates' + err) + //showError(err, 'Cannot check updates') + } + } + + let onchain + let nodeError = null + try { + onchain = await getState(api, pathname, initialState.offchain) + } catch (error) { + nodeError = error + } - try { - $STM_Config.add_notify_site = await checkUpdates() - } catch (err) { - console.error('Cannot check updates', err) clearTimeout(splashTimeout) closeSplash() - alert('Cannot check updates' + err) - } - - let onchain - let nodeError = null - try { - onchain = await getState(api, pathname, initialState.offchain) - } catch (error) { - nodeError = error - } - clearTimeout(splashTimeout) - closeSplash() + if (nodeError) { + showNodeError() + throw nodeError + } - if (nodeError) { - showNodeError() - throw nodeError + return onchain + } catch (err) { + if (isSettings()) { + console.error(err) + return + } + showError(err, 'initState') } - - return onchain } initState().then((onchain) => { diff --git a/app/app_cfg.js b/app/app_cfg.js new file mode 100644 index 000000000..d964f10b9 --- /dev/null +++ b/app/app_cfg.js @@ -0,0 +1,79 @@ +/* Only Mobile. Generated automatically. Do not edit. */ +module.exports = { + "app_version": "1.5.0", + "ws_connection_app": [ + { + "address": "wss://apibeta.golos.today/ws" + }, + { + "address": "wss://api.golos.id/ws" + }, + { + "address": "wss://api.aleksw.space/ws" + }, + { + "address": "wss://api-golos.blckchnd.com/ws" + } + ], + "ws_connection_exchange": "wss://apibeta.golos.today/ws", + "logo": { + "icon": "https://i.imgur.com/Q7GCdPf.png", + "title": "https://i.imgur.com/36zv8We.png" + }, + "images": { + "img_proxy_prefix": "https://devimages.golos.today", + "img_proxy_backup_prefix": "https://steemitimages.com", + "upload_image": "https://api.imgur.com/3/image", + "client_id": "6c09ebf8c548126" + }, + "wallet_service": { + "host": "https://devwallet.golos.today" + }, + "messenger_service": { + "host": "https://devchat.golos.app" + }, + "auth_service": { + "host": "https://dev.golos.app", + "custom_client": "blogs" + }, + "notify_service": { + "host": "https://devnotify.golos.app", + "host_ws": "wss://devnotify.golos.app/ws" + }, + "elastic_search": { + "url": "https://search.golos.today", + "login": "golosclient", + "password": "golosclient" + }, + "apidex_service": { + "host": "https://devapi-dex.golos.app", + "host_local": "https://devapi-dex.golos.app" + }, + "app_updater": { + "host": "https://devfiles.golos.app" + }, + "forums": { + "white_list": [ + "fm-golostalk", + "fm-prizmtalk", + "fm-graphenetalks" + ], + "fm-golostalk": { + "domain": "golostalk.com" + }, + "fm-prizmtalk": { + "domain": "prizmtalk.com" + }, + "fm-graphenetalks": { + "domain": "forum.gph.ai" + } + }, + "hidden_assets": { + "RUDEX": true, + "PRIZM": true, + "DOGECOIN": true, + "YMZEC": true, + "YMWMZ": true, + "YMBTC": true + } +} \ No newline at end of file diff --git a/app/assets/icons/badge-new-nobg.svg b/app/assets/icons/badge-new-nobg.svg new file mode 100644 index 000000000..2f8594884 --- /dev/null +++ b/app/assets/icons/badge-new-nobg.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/icons/badge-new.svg b/app/assets/icons/badge-new.svg new file mode 100644 index 000000000..c35612189 --- /dev/null +++ b/app/assets/icons/badge-new.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/app/android48x48.png b/app/assets/images/app/android48x48.png new file mode 100644 index 000000000..7bd064cdc Binary files /dev/null and b/app/assets/images/app/android48x48.png differ diff --git a/app/components/App.jsx b/app/components/App.jsx index f44da0d73..cd5bc3f5f 100644 --- a/app/components/App.jsx +++ b/app/components/App.jsx @@ -17,6 +17,7 @@ import URLLoader from 'app/components/elements/app/URLLoader'; import TooltipManager from 'app/components/elements/common/TooltipManager'; import user from 'app/redux/User'; import g from 'app/redux/GlobalReducer'; +import PushNotificationSaga from 'app/redux/services/PushNotificationSaga' import { Link } from 'react-router'; import resolveRoute from 'app/ResolveRoute'; import Dialogs from '@modules/Dialogs'; @@ -27,12 +28,17 @@ import MiniHeader from '@modules/MiniHeader'; import PageViewsCounter from '@elements/PageViewsCounter'; import ChainFailure from 'app/components/elements/ChainFailure' import DialogManager from 'app/components/elements/common/DialogManager'; +import LoadingIndicator from 'app/components/elements/LoadingIndicator' import NotifyPolling from 'app/components/elements/NotifyPolling' +import AppSettings, { openAppSettings } from 'app/components/pages/app/AppSettings' import { init as initAnchorHelper } from 'app/utils/anchorHelper'; import { authRegisterUrl, } from 'app/utils/AuthApiClient'; +import { fixRouteIfApp, reloadLocation, } from 'app/utils/app/RoutingUtils' +import { getShortcutIntent, onShortcutIntent } from 'app/utils/app/ShortcutUtils' import { APP_ICON, VEST_TICKER, } from 'app/client_config'; import session from 'app/utils/session' import { loadGrayHideSettings } from 'app/utils/ContentAccess' +import { withScreenSize } from 'app/utils/ScreenSize' import libInfo from 'app/JsLibHash.json' const GlobalStyle = createGlobalStyle` @@ -79,7 +85,10 @@ class App extends React.Component { p.new_visitor !== n.new_visitor || p.flash !== n.flash || this.state !== nextState || - p.nightmodeEnabled !== n.nightmodeEnabled + this.state.can_render !== nextState.can_render || + p.nightmodeEnabled !== n.nightmodeEnabled || + p.hideOrdersMe !== n.hideOrdersMe || + p.hideOrders !== n.hideOrders ); } @@ -104,15 +113,54 @@ class App extends React.Component { super(props) if (process.env.BROWSER) { this.loadDownvotedPrefs() + if (window.location.hash === '#app-settings') { + this.appSettings = true + } + window.appMounted = true } } - componentDidMount() { + async checkShortcutIntent() { + try { + const intent = await getShortcutIntent() + const intentId = intent.extras['gls.blogs.id'] + if (intent.extras['gls.blogs.hash'] === '#app-settings' && localStorage.getItem('processed_intent') !== intentId) { + this.appSettings = true + localStorage.setItem('processed_intent', intentId) + } + } catch (err) { + console.error('Cannot get shortcut intent', err) + } + } + +componentDidMount() { if (process.env.BROWSER) { console.log('ui-blogs version:', $STM_Config.ui_version); console.log('golos-lib-js version:', libInfo.version, 'hash:', libInfo.hash) } + if (process.env.MOBILE_APP) { + (async () => { + await this.checkShortcutIntent() + onShortcutIntent(intent => { + if (intent.extras['gls.blogs.hash'] === '#app-settings') { + openAppSettings() + } + }) + + fixRouteIfApp() + + document.addEventListener('pause', this.onPause) + document.addEventListener('resume', this.onResume) + + this.setState({ + can_render: true + }) + + this.stopService() + })() + } + const { nightmodeEnabled } = this.props; this.toggleBodyNightmode(nightmodeEnabled); @@ -164,6 +212,10 @@ class App extends React.Component { if (process.env.BROWSER) { window.removeEventListener('click', this.checkLeaveGolos); } + if (process.env.MOBILE_APP) { + document.addEventListener('pause', this.onPause) + document.addEventListener('resume', this.onResume) + } } componentDidUpdate(nextProps) { @@ -173,6 +225,51 @@ class App extends React.Component { } } + componentDidCatch(err, info) { + const errStr = (err && err.toString()) ? err.toString() : JSON.stringify(err) + const infoStr = (info && info.componentStack) || JSON.stringify(info) + if (confirm(';( Ошибка рендеринга. Продолжить работу?\n\n' + errStr + '\n' + infoStr)) { + reloadLocation('/') + return + } + throw err + } + + onPause = () => { + const { username } = this.props + const notifySess = localStorage.getItem('X-Session') + const notifyHost = $STM_Config.notify_service.host + if (username && notifySess) { + const settings = PushNotificationSaga.getScopePresets(username) + if (!settings.inBackground) { + console.warn('Notify - inBackground false, so do not starting service...') + return + } + if (!settings.bgPresets.length) { + console.warn('Notify - all background presets disabled, so do not starting service...') + return + } + const lastTake = 0 + cordova.exec((winParam) => { + console.log('pause ok', winParam) + }, (err) => { + console.error('pause err', err) + }, 'CorePlugin', 'startService', [username, notifySess, settings.bgPresets.join(','), lastTake, notifyHost]) + } + } + + onResume = () => { + this.stopService() + } + + stopService = () => { + cordova.exec((winParam) => { + console.log('resume ok', winParam) + }, (err) => { + console.error('resume err', err) + }, 'CorePlugin', 'stopService', []) + } + checkLogin = event => { if (event.key === 'autopost2') { if (!event.newValue) this.props.logoutUser(); @@ -250,14 +347,26 @@ class App extends React.Component { } render() { + if (process.env.MOBILE_APP && !this.state.can_render) { + return + } + const { location, params, children, flash, new_visitor, - nightmodeEnabled + nightmodeEnabled, + loggedIn, + } = this.props; + let { + hideOrders, + hideOrdersMe, } = this.props; + if (loggedIn) { + hideOrders = hideOrdersMe + } const route = resolveRoute(location.pathname); const lp = false; //location.pathname === '/'; @@ -268,7 +377,7 @@ class App extends React.Component { (params_keys.length === 2 && params_keys[0] === 'order' && params_keys[1] === 'category'); - const alert = this.props.error || flash.get('alert'); + const f_alert = this.props.error || flash.get('alert'); const warning = flash.get('warning'); const success = flash.get('success'); let callout = null; @@ -276,7 +385,7 @@ class App extends React.Component { const notifyTitle = $STM_Config.add_notify_site.title; const showInfoBox = $STM_Config.add_notify_site.show && this.isShowInfoBox($STM_Config.add_notify_site); - if (this.state.showCallout && (alert || warning || success)) { + if (this.state.showCallout && (f_alert || warning || success)) { callout = (
@@ -286,7 +395,7 @@ class App extends React.Component { this.setState({ showCallout: false }) } /> -

{alert || warning || success}

+

{f_alert || warning || success}

@@ -366,7 +475,7 @@ class App extends React.Component { const themeClass = nightmodeEnabled ? ' theme-dark' : ' theme-light'; - const isApp = process.env.IS_APP && location.pathname.startsWith('/__app_') + const isApp = location.pathname.startsWith('/__app_') || this.appSettings const noHeader = isApp const noFooter = isApp || location.pathname.startsWith('/submit') @@ -386,12 +495,13 @@ class App extends React.Component { {noHeader ? null : (miniHeader ? :
)}
{welcome_screen} {callout} - {children} + {this.appSettings ? : children} {noFooter ? null :
} @@ -417,21 +527,26 @@ App.propTypes = { loginUser: PropTypes.func.isRequired, logoutUser: PropTypes.func.isRequired, depositSteem: PropTypes.func.isRequired, - nightmodeEnabled: PropTypes.bool + nightmodeEnabled: PropTypes.bool, + loggedIn: PropTypes.bool }; export default connect( state => { let nightmodeEnabled = process.env.BROWSER ? localStorage.getItem('nightmodeEnabled') == 'true' || false : false + const currentUser = state.user.get('current') + return { error: state.app.get('error'), flash: state.offchain.get('flash'), + loggedIn: !!state.user.get('current'), new_visitor: - !state.user.get('current') && + !currentUser && !state.offchain.get('account') && state.offchain.get('new_visit'), - nightmodeEnabled: nightmodeEnabled + nightmodeEnabled: nightmodeEnabled, + username: currentUser && currentUser.get('username'), }; }, dispatch => ({ @@ -451,4 +566,4 @@ export default connect( dispatch(g.actions.fetchExchangeRates()); }, }) -)(App); +)(withScreenSize(App)) diff --git a/app/components/App.scss b/app/components/App.scss index 432731208..3f1cc5248 100644 --- a/app/components/App.scss +++ b/app/components/App.scss @@ -16,18 +16,9 @@ HTML { .App__content { margin-top: 2.5rem; - /* Small only */ - @media screen and (max-width: 39.9375em) { + &.ho { margin-top: 0; } - /* Medium only */ - @media screen and (min-width: 40em) and (max-width: 63.9375em) { - margin-top: 2.5rem; - } - /* Large and above */ - @media screen and (min-width: 64.063em) { - margin-top: 2.5rem; - } .callout { @include themify($themes) { color: themed('textColorPrimary'); diff --git a/app/components/all.scss b/app/components/all.scss index b6ba1da3f..54ad5ea64 100644 --- a/app/components/all.scss +++ b/app/components/all.scss @@ -80,6 +80,8 @@ @import "./modules/SponsorSubscription"; @import "./modules/BottomPanel"; @import "./modules/TopRightMenu"; +@import "./modules/MiniTopics"; +@import "./modules/Topics"; @import "./modules/Modals"; @import "./modules/PostForm/PostForm"; @@ -92,5 +94,4 @@ @import "./pages/PostsIndex"; @import "./pages/SubmitPost"; @import "./pages/TagsIndex"; -@import "./pages/Topics"; @import "./pages/UserProfile"; diff --git a/app/components/cards/PostFull.jsx b/app/components/cards/PostFull.jsx index 44e4f9f96..c79968b0f 100644 --- a/app/components/cards/PostFull.jsx +++ b/app/components/cards/PostFull.jsx @@ -214,7 +214,7 @@ class PostFull extends React.Component { const p = extractContent(immutableAccessor, postContent); const content = postContent.toJS(); - const { author, permlink, parent_author, parent_permlink, root_author, encrypted } = content; + const { author, permlink, parent_author, parent_permlink, root_author, encrypted, decrypt_fee } = content; const jsonMetadata = showReply ? null : p.json_metadata; let link = `/@${content.author}/${content.permlink}`; @@ -238,6 +238,7 @@ class PostFull extends React.Component { title, body, encrypted, + decrypt_fee: decrypt_fee ? Asset(decrypt_fee) : null, }; const APP_DOMAIN = $STM_Config.site_domain; diff --git a/app/components/cards/PostSummary.jsx b/app/components/cards/PostSummary.jsx index 1fb018deb..a39954193 100644 --- a/app/components/cards/PostSummary.jsx +++ b/app/components/cards/PostSummary.jsx @@ -5,6 +5,7 @@ import { connect } from 'react-redux' import { Link, browserHistory } from 'react-router' import {Map} from 'immutable' import tt from 'counterpart' +import { Asset } from 'golos-lib-js/lib/utils' import { CHANGE_IMAGE_PROXY_TO_STEEMIT_TIME } from 'app/client_config' import Author from 'app/components/elements/Author' @@ -33,6 +34,7 @@ import { addHighlight, unsubscribePost } from 'app/utils/NotifyApiClient' import { detransliterate } from 'app/utils/ParsersAndFormatters' import { proxifyImageUrl } from 'app/utils/ProxifyUrl' import { EncryptedStates } from 'app/utils/sponsors' +import { reloadLocation } from 'app/utils/app/RoutingUtils' import { walletUrl } from 'app/utils/walletUtils' function isLeftClickEvent(event) { @@ -44,8 +46,13 @@ function isModifiedEvent(event) { } function navigate(e, onClick, post, url, isForum, isSearch, warn) { - if (isForum || isSearch) + if (isForum || isSearch) { + if (process.env.MOBILE_APP) { + e.preventDefault() + reloadLocation(url) + } return; + } if (warn) { e.preventDefault() return @@ -282,7 +289,14 @@ class PostSummary extends React.Component { } else if (encrypted === EncryptedStates.unknown || encrypted === EncryptedStates.no_key) { encStub = tt('postsummary_jsx.no_decrypt_key') } else if (encrypted === EncryptedStates.no_sub) { - encStub = tt('postsummary_jsx.no_sub') + let decrypt_fee = content.get('decrypt_fee') + if (decrypt_fee) { + decrypt_fee = Asset(decrypt_fee) + if (decrypt_fee.amount > 0) { + encStub = tt('postsummary_jsx.for_sponsors') + } + } + if (!encStub) encStub = tt('postsummary_jsx.no_sub') } else if (encrypted && encrypted !== EncryptedStates.decrypted) { encStub = tt('postsummary_jsx.for_sponsors') } @@ -364,7 +378,7 @@ class PostSummary extends React.Component { if(gray) commentClasses.push('downvoted') // rephide if (loginBlurring) commentClasses.push('blurring') - total_search = total_search ? + total_search = total_search ? {tt('g.and_more_search_posts_COUNT', { COUNT: total_search })} : null diff --git a/app/components/cards/PostSummary.scss b/app/components/cards/PostSummary.scss index 362c909b5..b6c747e26 100644 --- a/app/components/cards/PostSummary.scss +++ b/app/components/cards/PostSummary.scss @@ -1,5 +1,17 @@ ul.PostsList__summaries { margin-left: 0; + + .search-header { + font-size: 1rem; + } +} +/* Small only */ +@media screen and (max-width: 39.9375em) { + ul.PostsList__summaries { + .search-header { + font-size: 0.75rem; + } + } } .PostSummary { diff --git a/app/components/dialogs/AddImageDialog/index.jsx b/app/components/dialogs/AddImageDialog/index.jsx index b42a1ec4d..2c3c3e5f0 100644 --- a/app/components/dialogs/AddImageDialog/index.jsx +++ b/app/components/dialogs/AddImageDialog/index.jsx @@ -14,9 +14,11 @@ export default class AddImageDialog extends React.PureComponent { }; componentDidMount() { - const linkInput = document.getElementsByClassName('AddImageDialog__link-input')[0]; - if (linkInput) - linkInput.focus(); + if (!process.env.MOBILE_APP) { + const linkInput = document.getElementsByClassName('AddImageDialog__link-input')[0] + if (linkInput) + linkInput.focus() + } } render() { @@ -49,6 +51,7 @@ export default class AddImageDialog extends React.PureComponent { block className="AddImageDialog__link-input" placeholder="https://" + enterkeyhint="enter" onKeyDown={this._onInputKeyDown} />
diff --git a/app/components/elements/DropdownMenu.jsx b/app/components/elements/DropdownMenu.jsx index 2a4dfabf9..5d69d4187 100644 --- a/app/components/elements/DropdownMenu.jsx +++ b/app/components/elements/DropdownMenu.jsx @@ -14,7 +14,8 @@ export default class DropdownMenu extends React.Component { title: PropTypes.string, href: PropTypes.string, el: PropTypes.string.isRequired, - noArrow: PropTypes.bool + noArrow: PropTypes.bool, + arrowCenter: PropTypes.bool }; constructor(props) { @@ -68,12 +69,12 @@ export default class DropdownMenu extends React.Component { } render() { - const {el, items, selected, children, className, title, href, noArrow} = this.props; + const {el, items, selected, children, className, title, href, noArrow, arrowCenter} = this.props; const hasDropdown = items.length > 0 let entry = children || {this.getSelectedLabel(items, selected)} - {hasDropdown && !noArrow && } + {hasDropdown && !noArrow && } if(hasDropdown) entry = {entry} diff --git a/app/components/elements/EncryptedStub.jsx b/app/components/elements/EncryptedStub.jsx index 1d61c29b8..c6b185d5e 100644 --- a/app/components/elements/EncryptedStub.jsx +++ b/app/components/elements/EncryptedStub.jsx @@ -5,6 +5,7 @@ import { Asset } from 'golos-lib-js/lib/utils' import LoadingIndicator from 'app/components/elements/LoadingIndicator' import transaction from 'app/redux/Transaction' +import { checkAllowed, AllowTypes } from 'app/utils/Allowance' import LinkEx from 'app/utils/LinkEx' import { EncryptedStates, makeOid } from 'app/utils/sponsors' @@ -52,6 +53,10 @@ class EncryptedStub extends React.Component { this.props.fetchState() }, 1500) }, (err) => { + err = err.message || err + if (err && err.includes('Account does not have sufficient funds')) { + err = tt('transfer_jsx.insufficient_funds') + } this.setState({ submitting: false, err }) }) } @@ -69,47 +74,113 @@ class EncryptedStub extends React.Component { } + onDonateClick = (e, decrypt_fee) => { + e.preventDefault() + + const { dis, username, } = this.props + + const author = dis.get('author') + const permlink = dis.get('permlink') + + this.setState({ submitting: true, err: null }) + + this.props.donate({ + username, + to: author, + permlink, + amount: decrypt_fee, + successCallback: () => { + this.setState({ submitting: false }) + }, + errorCallback: (err) => { + console.error(err) + err = err.message || err + if (err && err.includes('Account does not have sufficient funds')) { + err = tt('transfer_jsx.insufficient_funds') + } + + this.setState({ submitting: false, err }) + } + }) + } + + _renderDonate = (decrypt_fee, isMy) => { + if (isMy || !decrypt_fee) return null + decrypt_fee = Asset(decrypt_fee) + if (decrypt_fee.eq(0)) return null + return
+ {tt('poststub.you_can_decrypt_just_this_post')} + {decrypt_fee.floatString}.
+ +
+ } + + onDeleteClick = (e) => { + e.preventDefault() + + const { dis, } = this.props + + const author = dis.get('author') + const permlink = dis.get('permlink') + + this.props.deletePost(author, permlink) + } + render() { const { dis, encrypted, username } = this.props const author = dis.get('author') - if (encrypted === EncryptedStates.no_auth) { + const isMy = author === username + + const btn = isMy &&
+ + if (this.state.submitting) { + return + } + + if ((encrypted === EncryptedStates.no_sponsor && !username) || encrypted === EncryptedStates.no_auth) { return (
- {this._renderAuthor(tt('poststub.for_sponsors'), author)} - {tt('poststub.login_to_become_sponsor')} + {this._renderAuthor(tt('poststub.for_sponsors'), author)} + {tt('poststub.login_to_become_sponsor')}
) } else if (encrypted === EncryptedStates.no_sponsor) { const sub = dis.get('encrypted_sub') - - if (!username) { - return (
- {this._renderAuthor(tt('poststub.for_sponsors'), author)} - {tt('poststub.login_to_become_sponsor')} -
) - } + const encrypted_decrypt_fee = dis.get('encrypted_decrypt_fee') return (
{this._renderAuthor(tt('poststub.for_sponsors'), author)} {this._renderSub(sub)} {this._renderButton(sub, tt('poststub.become_sponsor'))} + {this._renderDonate(encrypted_decrypt_fee, isMy)}
) } else if (encrypted === EncryptedStates.inactive) { const sub = dis.get('encrypted_sub') + const encrypted_decrypt_fee = dis.get('encrypted_decrypt_fee') return (
{this._renderAuthor(tt('poststub.sponsorship_expired'), author)} {this._renderSub(sub)} {this._renderButton(sub, tt('poststub.prolong_sponsorship'))} + {this._renderDonate(encrypted_decrypt_fee, isMy)}
) } else if (encrypted === EncryptedStates.no_key) { - return
{tt('postsummary_jsx.no_decrypt_key')}
+ return
{tt('postsummary_jsx.no_decrypt_key')}{btn}
} else if (encrypted === EncryptedStates.no_sub) { - return
{tt('postsummary_jsx.no_sub')}
+ const encrypted_decrypt_fee = dis.get('encrypted_decrypt_fee') + + const donateWay = this._renderDonate(encrypted_decrypt_fee, isMy) + + return
{!donateWay && tt('postsummary_jsx.no_sub')} + {donateWay} +
} else if (encrypted === EncryptedStates.wrong_format) { - return
{tt('postsummary_jsx.wrong_format')}
+ return
{tt('postsummary_jsx.wrong_format')}{btn}
} - return
{tt('postsummary_jsx.no_decrypt_key')}
+ return
{tt('postsummary_jsx.no_decrypt_key')}{btn}
} } @@ -143,6 +214,55 @@ export default connect( } })) }, + deletePost(author, permlink) { + dispatch( + transaction.actions.broadcastOperation({ + type: 'delete_comment', + operation: { author, permlink }, + confirm: tt('g.are_you_sure'), + }) + ); + }, + donate: async ({ + username, to, permlink, amount, successCallback, errorCallback + }) => { + let operation = { + from: username, to, amount: amount.toString() + } + + operation.memo = { + app: 'golos-blog', version: 1, comment: '', + target: { + author: to, permlink + } + } + + let trx = [ + ['donate', operation] + ] + let aTypes = [ + AllowTypes.transfer + ] + + const onSuccess = () => { + const pathname = window.location.pathname + dispatch({type: 'FETCH_STATE', payload: {pathname}}) + successCallback() + } + + let confirm + const tipAmount = Asset(operation.amount) + const blocking = await checkAllowed(operation.from, [operation.to], tipAmount, aTypes) + if (blocking.error) { + errorCallback(blocking.error) + return + } + confirm = blocking.confirm + + dispatch(transaction.actions.broadcastOperation({ + type: 'donate', username, trx, confirm, successCallback: onSuccess, errorCallback + })) + }, fetchState: () => { const pathname = window.location.pathname dispatch({type: 'FETCH_STATE', payload: {pathname}}) diff --git a/app/components/elements/Icon.jsx b/app/components/elements/Icon.jsx index b013b9dbb..b8b3c1215 100644 --- a/app/components/elements/Icon.jsx +++ b/app/components/elements/Icon.jsx @@ -31,6 +31,7 @@ const icons = new Map([ ['printer', require('app/assets/icons/printer.svg')], ['search', require('app/assets/icons/search.svg')], ['menu', require('app/assets/icons/menu.svg')], + ['badge-new', require('app/assets/icons/badge-new-nobg.svg')], ['voter', require('app/assets/icons/voter.svg')], ['voters', require('app/assets/icons/voters.svg')], ['empty', require('app/assets/icons/empty.svg')], diff --git a/app/components/elements/Icon.scss b/app/components/elements/Icon.scss index 65d755d91..ee8287245 100644 --- a/app/components/elements/Icon.scss +++ b/app/components/elements/Icon.scss @@ -74,3 +74,8 @@ margin: 24px 0 24px 0; } +.badge-new { + color: red; + margin-left: 2px; + margin-right: 3px; +} diff --git a/app/components/elements/LocaleSelect.jsx b/app/components/elements/LocaleSelect.jsx index 306e9d0ea..e2c27156c 100644 --- a/app/components/elements/LocaleSelect.jsx +++ b/app/components/elements/LocaleSelect.jsx @@ -193,12 +193,6 @@ export default connect((state, props) => { locale, }; }, dispatch => ({ - uploadImage: (file, progress) => { - dispatch({ - type: 'user/UPLOAD_IMAGE', - payload: {file, progress}, - }) - }, changeLanguage: (language) => { dispatch(user.actions.changeLanguage(language)) }, diff --git a/app/components/elements/NewsPopups.jsx b/app/components/elements/NewsPopups.jsx index a153590bc..583097187 100644 --- a/app/components/elements/NewsPopups.jsx +++ b/app/components/elements/NewsPopups.jsx @@ -5,6 +5,7 @@ import { api } from 'golos-lib-js' import CloseButton from 'react-foundation-components/lib/global/close-button' +import Icon from '@elements/Icon'; import user from 'app/redux/User' const APP_REMINDER_INTERVAL = 30*24*60*60*1000 @@ -94,8 +95,7 @@ class NewsPopups extends React.Component { } showAppReminder = () => { - if (process.env.IS_APP || typeof(localStorage) === 'undefined' - || location.pathname.startsWith('/submit')) { + if (process.env.IS_APP || typeof(localStorage) === 'undefined') { return false } const now = Date.now() @@ -105,6 +105,10 @@ class NewsPopups extends React.Component { } render() { + if (process.env.BROWSER && location.pathname.startsWith('/submit')) { + return null + } + const { news,hiddenNews } = this.state let appReminder = null @@ -117,6 +121,8 @@ class NewsPopups extends React.Component { }} /> {tt('app_reminder.text')} + + {tt('app_reminder.text2')} } diff --git a/app/components/elements/QrCode.jsx b/app/components/elements/QrCode.jsx index 4b37932c9..db9bf76d2 100644 --- a/app/components/elements/QrCode.jsx +++ b/app/components/elements/QrCode.jsx @@ -9,8 +9,12 @@ export default class ReactQR extends Component { } render() { - var pngBuffer = qrImage.imageSync(this.props.text, { type: 'png', margin: 1 }) - var dataURI = 'data:image/png;base64,' + pngBuffer.toString('base64') + let opts = { type: 'png', margin: 1 } + if (this.props.size) { + opts.size = this.props.size + } + let pngBuffer = qrImage.imageSync(this.props.text, opts) + let dataURI = 'data:image/png;base64,' + pngBuffer.toString('base64') return ( ) diff --git a/app/components/elements/ReplyEditor.jsx b/app/components/elements/ReplyEditor.jsx index 320e90f0e..895d6b71f 100644 --- a/app/components/elements/ReplyEditor.jsx +++ b/app/components/elements/ReplyEditor.jsx @@ -861,7 +861,7 @@ export default formId => uploadImage: (file, progress) => { dispatch({ type: 'user/UPLOAD_IMAGE', - payload: { file, progress, useGolosImages: true, }, + payload: { file, progress, }, }); }, reply: replyAction(dispatch, remarkable), diff --git a/app/components/elements/VerticalMenu.jsx b/app/components/elements/VerticalMenu.jsx index e5ea20848..0b871074a 100644 --- a/app/components/elements/VerticalMenu.jsx +++ b/app/components/elements/VerticalMenu.jsx @@ -4,6 +4,10 @@ import PropTypes from 'prop-types' import Icon from 'app/components/elements/Icon' import LinkEx from 'app/utils/LinkEx' +const isSepar = (item) => { + return item && item.value === '-' +} + export default class VerticalMenu extends React.Component { static propTypes = { items: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -32,10 +36,21 @@ export default class VerticalMenu extends React.Component { {title &&
  • {title}
  • } {description &&
  • {description}
  • } {items.map((i, k) => { + const prev = items[k - 1] + const next = items[k + 1] if(i.value === hideValue) return null + if (isSepar(i)) { + return
    + } const iconSize = i.iconSize || '1x' const target = i.target - return
  • + let className = '' + if (isSepar(prev)) { + className += ' padd-top' + } else if (isSepar(next)) { + className += ' padd-bottom' + } + return
  • {i.link ? {i.icon && }{i.label ? i.label : i.value} {i.data && {i.data}} diff --git a/app/components/elements/VerticalMenu.scss b/app/components/elements/VerticalMenu.scss index 83ebd09c2..20e486ed1 100644 --- a/app/components/elements/VerticalMenu.scss +++ b/app/components/elements/VerticalMenu.scss @@ -24,6 +24,14 @@ letter-spacing: 0.4px; } + > li.padd-top > a { + padding-top: 1rem; + } + + > li.padd-bottom > a { + padding-bottom: 1rem; + } + > li > a:hover { background-color: #f0f0f0; } @@ -52,6 +60,12 @@ border-bottom: 1px solid $light-gray; } + > hr { + width: 90%; + margin-top: 0px; + margin-bottom: 0px; + } + &_nav-profile { width: 262px; diff --git a/app/components/elements/VotesAndComments.jsx b/app/components/elements/VotesAndComments.jsx index c6479fee3..cc9e1b9d0 100644 --- a/app/components/elements/VotesAndComments.jsx +++ b/app/components/elements/VotesAndComments.jsx @@ -2,9 +2,11 @@ import React from 'react'; import PropTypes from 'prop-types' import { Link } from 'react-router'; import { connect } from 'react-redux'; +import tt from 'counterpart'; + import Icon from 'app/components/elements/Icon'; import shouldComponentUpdate from 'app/utils/shouldComponentUpdate' -import tt from 'counterpart'; +import { reloadLocation } from 'app/utils/app/RoutingUtils' class VotesAndComments extends React.Component { @@ -29,6 +31,10 @@ class VotesAndComments extends React.Component { const { isForum, fromSearch, commentsLink } = this.props; if (isForum || fromSearch) { event.preventDefault(); + if (process.env.MOBILE_APP) { + reloadLocation(commentsLink) + return + } window.open(commentsLink, '_blank'); } }; diff --git a/app/components/elements/app/LoginAppReminder.jsx b/app/components/elements/app/LoginAppReminder.jsx new file mode 100644 index 000000000..534c11033 --- /dev/null +++ b/app/components/elements/app/LoginAppReminder.jsx @@ -0,0 +1,51 @@ + +import React, { Component } from 'react' +import tt from 'counterpart' + +import Icon from '@elements/Icon' +import QRCode from 'app/components/elements/QrCode' + +class LoginAppReminder extends Component { + state = { showAndroid: false } + + showForAndroid = (e) => { + e.preventDefault() + this.setState({ + showAndroid: !this.state.showAndroid + }) + } + + render() { + const winUrl = 'https://files.golos.app/api/exe/desktop/windows/latest' + const linuxUrl = 'https://files.golos.app/api/exe/desktop/linux/latest' + const androidUrl = 'https://files.golos.app/api/exe/blogs/android/latest' + + const { showAndroid } = this.state + + const androidTitle = (tt('login_app_reminder.title') + 'Android') + + return
    + {tt('login_app_reminder.or_download_for')} + + + + + + + + + + + {showAndroid ? + : null} +
    + } +} + +export default LoginAppReminder diff --git a/app/components/elements/app/LoginAppReminder.scss b/app/components/elements/app/LoginAppReminder.scss new file mode 100644 index 000000000..fbd829316 --- /dev/null +++ b/app/components/elements/app/LoginAppReminder.scss @@ -0,0 +1,2 @@ +.LoginAppReminder { +} diff --git a/app/components/elements/app/URLLoader.jsx b/app/components/elements/app/URLLoader.jsx index 627a4a836..5e188cb0c 100644 --- a/app/components/elements/app/URLLoader.jsx +++ b/app/components/elements/app/URLLoader.jsx @@ -3,6 +3,9 @@ import { browserHistory } from 'react-router' class URLLoader extends React.Component { componentDidMount() { + if (process.env.MOBILE_APP) { + return + } window.appNavigation.onRouter((url) => { try { let parsed = new URL(url) diff --git a/app/components/elements/donate/Donate.scss b/app/components/elements/donate/Donate.scss index 70899b537..a5ae9f54b 100644 --- a/app/components/elements/donate/Donate.scss +++ b/app/components/elements/donate/Donate.scss @@ -21,6 +21,10 @@ &.micro { font-size: 70%; } + + &.small { + width: 120px; + } } .PresetSelector { diff --git a/app/components/elements/donate/TipAssetList.jsx b/app/components/elements/donate/TipAssetList.jsx index 56265e077..15f99a622 100644 --- a/app/components/elements/donate/TipAssetList.jsx +++ b/app/components/elements/donate/TipAssetList.jsx @@ -11,6 +11,7 @@ class TipAssetList extends React.Component { currentAccount: PropTypes.object.isRequired, uias: PropTypes.object, onChange: PropTypes.func.isRequired, + small: PropTypes.bool, } onSelected = (e) => { @@ -26,7 +27,7 @@ class TipAssetList extends React.Component { } render() { - const { currentAccount, currentBalance } = this.props + const { currentAccount, currentBalance, small } = this.props const golosBalance = Asset(currentAccount.get('tip_balance')) let tipBalanceValue = currentBalance && currentBalance.toString(0) @@ -66,7 +67,7 @@ class TipAssetList extends React.Component { ' mini' : '' return ( -
    +
    {tt('token_names.TIP_TOKEN')}:
    {tipBalanceValue}
    diff --git a/app/components/elements/postEditor/CommentFooter/index.jsx b/app/components/elements/postEditor/CommentFooter/index.jsx index 6229d88fa..14a432970 100644 --- a/app/components/elements/postEditor/CommentFooter/index.jsx +++ b/app/components/elements/postEditor/CommentFooter/index.jsx @@ -2,12 +2,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import cn from 'classnames'; import tt from 'counterpart'; + import Button from 'app/components/elements/common/Button'; import Hint from 'app/components/elements/common/Hint'; import EmojiPicker from 'app/components/elements/EmojiPicker'; import './index.scss'; +import { withScreenSize } from 'app/utils/ScreenSize' -export default class CommentFooter extends React.PureComponent { +class CommentFooter extends React.PureComponent { static propTypes = { editMode: PropTypes.bool, postDisabled: PropTypes.bool, @@ -38,9 +40,11 @@ export default class CommentFooter extends React.PureComponent { }; render() { - const { editMode, postDisabled } = this.props; + const { editMode, postDisabled, isS } = this.props; const { temporaryErrorText } = this.state; + const ePicker = + return (
    - + {(!isS) && ePicker}
    @@ -86,3 +90,5 @@ export default class CommentFooter extends React.PureComponent { }, 5000); } } + +export default withScreenSize(CommentFooter) diff --git a/app/components/elements/postEditor/EditorSwitcher/EditorSwitcher.jsx b/app/components/elements/postEditor/EditorSwitcher/EditorSwitcher.jsx index 2dc9189d8..cd9dfdd18 100644 --- a/app/components/elements/postEditor/EditorSwitcher/EditorSwitcher.jsx +++ b/app/components/elements/postEditor/EditorSwitcher/EditorSwitcher.jsx @@ -8,10 +8,16 @@ export default class EditorSwitcher extends PureComponent { items: PropTypes.array.isRequired, activeId: PropTypes.number, onChange: PropTypes.func.isRequired, + isS: PropTypes.bool, }; render() { - const { items, activeId } = this.props; + const { items, activeId, isS } = this.props; + + let fontSize + if (!isS) { + fontSize = '18px' + } return (
    @@ -21,6 +27,7 @@ export default class EditorSwitcher extends PureComponent { className={cn('EditorSwitcher__item', { EditorSwitcher__item_active: item.id === activeId, })} + style={{ fontSize }} onClick={ item.id === activeId ? null diff --git a/app/components/elements/postEditor/EditorSwitcher/EditorSwitcher.scss b/app/components/elements/postEditor/EditorSwitcher/EditorSwitcher.scss index 3322a2ae4..31ede9f06 100644 --- a/app/components/elements/postEditor/EditorSwitcher/EditorSwitcher.scss +++ b/app/components/elements/postEditor/EditorSwitcher/EditorSwitcher.scss @@ -13,7 +13,6 @@ white-space: nowrap; letter-spacing: 1.4px; color: #b7b7b9; - font-size: 18px; transition: color 0.1s; flex-shrink: 0; cursor: pointer; diff --git a/app/components/elements/postEditor/MarkdownEditorToolbar/index.jsx b/app/components/elements/postEditor/MarkdownEditorToolbar/index.jsx index eaeb75497..6fe6947f0 100644 --- a/app/components/elements/postEditor/MarkdownEditorToolbar/index.jsx +++ b/app/components/elements/postEditor/MarkdownEditorToolbar/index.jsx @@ -1,17 +1,20 @@ import React from 'react'; import cn from 'classnames'; import tt from 'counterpart'; + import KEYS from 'app/utils/keyCodes'; import Icon from 'app/components/elements/Icon'; import DialogManager from 'app/components/elements/common/DialogManager'; import AddImageDialog from '../../../dialogs/AddImageDialog'; import LinkOptionsDialog from '../../../dialogs/LinkOptionsDialog'; import plusSvg from 'app/assets/icons/editor-toolbar/plus.svg'; +import { withScreenSize } from 'app/utils/ScreenSize' const MAX_HEADING = 4; const TOOLBAR_OFFSET = 7; const TOOLBAR_WIDTH = 468; const TOOLBAR_COMMENT_WIDTH = 336; +const TOOLBAR_SMALL_WIDTH = 300; const MIN_TIP_OFFSET = 29; const PLUS_ACTIONS = [ @@ -34,7 +37,7 @@ const PLUS_ACTIONS = [ }, ]; -export default class MarkdownEditorToolbar extends React.PureComponent { +class MarkdownEditorToolbar extends React.PureComponent { constructor(props) { super(props); @@ -79,12 +82,12 @@ export default class MarkdownEditorToolbar extends React.PureComponent { } render() { - const { commentMode } = this.props; + const { commentMode, isS, } = this.props; const { newLineHelper } = this.state; return (
    @@ -95,15 +98,17 @@ export default class MarkdownEditorToolbar extends React.PureComponent { } _renderToolbar() { - const { SM, commentMode } = this.props; + const { SM, commentMode, isS } = this.props; const { state, toolbarPosition, toolbarShow } = this.state; const { root } = this.refs; const editor = this._editor; - const toolbarWidth = commentMode + const toolbarWidth = isS + ? TOOLBAR_SMALL_WIDTH + : (commentMode ? TOOLBAR_COMMENT_WIDTH - : TOOLBAR_WIDTH; + : TOOLBAR_WIDTH) const style = { width: toolbarWidth, @@ -139,9 +144,22 @@ export default class MarkdownEditorToolbar extends React.PureComponent { style.left = Math.round(left); } + //alert(JSON.stringify(rootPos) + ' ' + style.left) } - const actions = [ + const largeActions = [ + { + icon: 'picture', + tooltip: tt('editor_toolbar.add_image'), + onClick: this._addImage, + }, + { + icon: 'video', + tooltip: tt('editor_toolbar.add_video'), + onClick: this._drawVideo, + } + ] + let actions = [ { active: state.bold, icon: 'bold', @@ -192,17 +210,10 @@ export default class MarkdownEditorToolbar extends React.PureComponent { tooltip: tt('editor_toolbar.add_link'), onClick: this._draw, }, - { - icon: 'picture', - tooltip: tt('editor_toolbar.add_image'), - onClick: this._addImage, - }, - { - icon: 'video', - tooltip: tt('editor_toolbar.add_video'), - onClick: this._drawVideo, - }, - ]; + ] + if (!isS) { + actions = [...actions, ...largeActions] + } return (
    @@ -356,6 +369,9 @@ export default class MarkdownEditorToolbar extends React.PureComponent { toolbarPosition.top = Math.round(bound.top); toolbarPosition.left = Math.round(bound.left + bound.width / 2); } + if (process.env.MOBILE_APP && !toolbarPosition.left) { + toolbarPosition.left = 0 + } newState.toolbarShow = true; newState.toolbarPosition = toolbarPosition; @@ -699,3 +715,5 @@ export default class MarkdownEditorToolbar extends React.PureComponent { } }; } + +export default withScreenSize(MarkdownEditorToolbar) diff --git a/app/components/elements/postEditor/MarkdownEditorToolbar/index.scss b/app/components/elements/postEditor/MarkdownEditorToolbar/index.scss index 10dd4b3b2..0ab0479a8 100644 --- a/app/components/elements/postEditor/MarkdownEditorToolbar/index.scss +++ b/app/components/elements/postEditor/MarkdownEditorToolbar/index.scss @@ -55,6 +55,10 @@ color: #0078c4; } + &_small { + width: 30px; + } + & > svg { vertical-align: middle; width: unset; diff --git a/app/components/elements/postEditor/PostFooter/PostFooter.jsx b/app/components/elements/postEditor/PostFooter/PostFooter.jsx index e4e1fc14c..08129e226 100644 --- a/app/components/elements/postEditor/PostFooter/PostFooter.jsx +++ b/app/components/elements/postEditor/PostFooter/PostFooter.jsx @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import cn from 'classnames'; import tt from 'counterpart'; + import TagInput from 'app/components/elements/postEditor/TagInput'; import TagsEditLine from 'app/components/elements/postEditor/TagsEditLine'; import PostOptions from 'app/components/elements/postEditor/PostOptions/PostOptions'; @@ -20,8 +21,10 @@ export default class PostFooter extends PureComponent { postDisabled: PropTypes.bool, postEncrypted: PropTypes.bool, disabledHint: PropTypes.string, + isS: PropTypes.bool, onPayoutTypeChange: PropTypes.func.isRequired, onCurationPercentChange: PropTypes.func.isRequired, + onDecryptFeeChange: PropTypes.func.isRequired, onTagsChange: PropTypes.func.isRequired, onPost: PropTypes.func.isRequired, onResetClick: PropTypes.func.isRequired, @@ -71,7 +74,8 @@ export default class PostFooter extends PureComponent { const { editMode, postEncrypted } = this.props if (editMode) { - this.props.onPost(postEncrypted ? VISIBLE_TYPES.ONLY_SPONSORS : VISIBLE_TYPES.ALL) + this.props.onPost(postEncrypted ? VISIBLE_TYPES.ONLY_SPONSORS : VISIBLE_TYPES.ALL, + this.props.decryptFee) return } @@ -105,15 +109,18 @@ export default class PostFooter extends PureComponent { } _renderVisibleOptions = (postDisabled) => { + const { decryptFee } = this.props + const onClick = (e) => { e.preventDefault() if (!postDisabled) - this.props.onPost(parseInt(e.target.parentNode.dataset.value)) + this.props.onPost(parseInt(e.target.parentNode.dataset.value), + decryptFee) } const visibleItems = [ - {link: '#', label: tt('post_editor.visible_option_all'), value: VISIBLE_TYPES.ALL, onClick }, - {link: '#', label: tt('post_editor.visible_option_onlyblog'), value: VISIBLE_TYPES.ONLY_BLOG, onClick }, + {disabled: decryptFee && decryptFee.amount > 0, link: '#', label: tt('post_editor.visible_option_all'), value: VISIBLE_TYPES.ALL, onClick }, + {disabled: decryptFee && decryptFee.amount > 0, link: '#', label: tt('post_editor.visible_option_onlyblog'), value: VISIBLE_TYPES.ONLY_BLOG, onClick }, {link: '#', label: tt('post_editor.visible_option_onlysponsors'), value: VISIBLE_TYPES.ONLY_SPONSORS, onClick }, ] @@ -134,6 +141,7 @@ export default class PostFooter extends PureComponent { categories, postDisabled, disabledHint, + isS, } = this.props; const { temporaryErrorText, singleLine, showVisibleOptions } = this.state; @@ -175,6 +183,8 @@ export default class PostFooter extends PureComponent { editMode={editMode} onPayoutChange={this.props.onPayoutTypeChange} onCurationPercentChange={this.props.onCurationPercentChange} + decryptFee={this.props.decryptFee} + onDecryptFeeChange={this.props.onDecryptFeeChange} />) const buttons = (
    @@ -231,8 +241,10 @@ export default class PostFooter extends PureComponent { >
    - {!editMode && + { categories.map((cat) => { return ; @@ -249,7 +261,8 @@ export default class PostFooter extends PureComponent { onChange={onTagsChange} /> ) : null} - +
    {isMobile ? null : options} {isMobile ? null : buttons} diff --git a/app/components/elements/postEditor/PostFooter/PostFooter.scss b/app/components/elements/postEditor/PostFooter/PostFooter.scss index b618474e0..bc0797b05 100644 --- a/app/components/elements/postEditor/PostFooter/PostFooter.scss +++ b/app/components/elements/postEditor/PostFooter/PostFooter.scss @@ -23,6 +23,11 @@ border-radius: 20px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.18); font-size: 15px; + + &.small { + width: 150px; + font-size: 14px; + } } &__cat { diff --git a/app/components/elements/postEditor/PostOptions/PostOptions.jsx b/app/components/elements/postEditor/PostOptions/PostOptions.jsx index 71b3cb476..e94e8f365 100644 --- a/app/components/elements/postEditor/PostOptions/PostOptions.jsx +++ b/app/components/elements/postEditor/PostOptions/PostOptions.jsx @@ -4,6 +4,7 @@ import cn from 'classnames'; import tt from 'counterpart'; import { isNil } from 'ramda'; import { api } from 'golos-lib-js'; +import { Asset, AssetEditor } from 'golos-lib-js/lib/utils' import {connect} from 'react-redux'; import styled from 'styled-components'; @@ -32,16 +33,20 @@ const CuratorValue = styled.b` font-weight: 500; `; +const DEFAULT_DECRYPT_FEE = '100.000 GOLOS' + class PostOptions extends React.PureComponent { static propTypes = { nsfw: PropTypes.bool.isRequired, payoutType: PropTypes.number.isRequired, curationPercent: PropTypes.number.isRequired, + decryptFee: PropTypes.object, editMode: PropTypes.bool, onNsfwClick: PropTypes.func.isRequired, onPayoutChange: PropTypes.func.isRequired, onCurationPercentChange: PropTypes.func.isRequired, + onDecryptFeeChange: PropTypes.func.isRequired, }; constructor(props) { @@ -51,9 +56,12 @@ class PostOptions extends React.PureComponent { this.state = { showCoinMenu: false, + showLockMenu: false, minCurationPercent: 0, maxCurationPercent: 0, + + decryptFeeEdit: AssetEditor(DEFAULT_DECRYPT_FEE), }; } @@ -66,7 +74,10 @@ class PostOptions extends React.PureComponent { } render() { - const { showCoinMenu, curatorPercent, minCurationPercent, maxCurationPercent } = this.state; + + const { editMode, decryptFee } = this.props + const { showCoinMenu, showLockMenu, curatorPercent, minCurationPercent, maxCurationPercent, decryptFeeToSave } = this.state; + const hasDecryptFee = (decryptFee && decryptFee.amount > 0) return (
    @@ -97,6 +108,22 @@ class PostOptions extends React.PureComponent { {showCoinMenu ? this._renderCoinMenu() : null} + {(!editMode || hasDecryptFee) ? + + + + {showLockMenu ? this._renderLockMenu() : null} + : null}
    ); } @@ -123,6 +150,45 @@ class PostOptions extends React.PureComponent { ); } + onDecryptFeeChange = (e) => { + const decryptFeeEdit = this.state.decryptFeeEdit.withChange(e.target.value) + if (decryptFeeEdit.asset.amount < 0) return + if (this.props.editMode && decryptFeeEdit.asset.amount == 0) return + + this.setState({ + decryptFeeEdit + }) + } + + _lockOkClick = (e) => { + e.preventDefault() + const { decryptFeeEdit } = this.state + this.setState({ + decryptFeeToSave: true, + showLockMenu: false + }, () => { + this.props.onDecryptFeeChange(decryptFeeEdit.asset.clone()) + }) + } + + _renderLockMenu() { + let { decryptFeeEdit } = this.state + return ( + +
    + {tt('post_editor.lock_hint')}: +
    +
    + + + GOLOS + +
    + +
    + ); + } + _onCoinClick = () => { this.setState( { @@ -139,6 +205,36 @@ class PostOptions extends React.PureComponent { ); }; + _onLockClick = () => { + const { decryptFee, editMode } = this.props + if (!editMode && decryptFee && decryptFee.amount > 0) { + this.setState({ + decryptFee: Asset('0.000 GOLOS'), + decryptFeeEdit: AssetEditor(DEFAULT_DECRYPT_FEE) + }, () => { + this.props.onDecryptFeeChange(this.state.decryptFee.clone()) + }) + return + } + const { showLockMenu, } = this.state + if (!showLockMenu && decryptFee && decryptFee.amount > 0) { + this.setState({ + decryptFeeEdit: AssetEditor(decryptFee.clone()) + }) + } + this.setState( + { + showLockMenu: !showLockMenu, + }, + () => { + if (this.state.showLockMenu && !this._onAwayClickListen) { + window.addEventListener('mousedown', this._onAwayClick); + this._onAwayClickListen = true; + } + } + ); + }; + _onCoinModeChange = coinMode => { this.props.onPayoutChange(coinMode); }; @@ -157,12 +253,25 @@ class PostOptions extends React.PureComponent { } }, 50); } + if (!this._popupLock || !this._popupLock.contains(e.target)) { + setTimeout(() => { + if (!this._unmount) { + this.setState({ + showLockMenu: false, + }); + } + }, 50); + } }; _popupPayoutRef = el => { this._popupPayout = el } + _popupLockRef = el => { + this._popupLock = el + } + componentDidMount() { // FIXME Dirty hack // retry api call diff --git a/app/components/elements/postEditor/TagInput/index.jsx b/app/components/elements/postEditor/TagInput/index.jsx index aec329df9..7f66388b1 100644 --- a/app/components/elements/postEditor/TagInput/index.jsx +++ b/app/components/elements/postEditor/TagInput/index.jsx @@ -12,6 +12,7 @@ export default class TagInput extends React.PureComponent { tags: PropTypes.array.isRequired, className: PropTypes.string, onChange: PropTypes.func.isRequired, + isS: PropTypes.bool, }; constructor(props) { @@ -31,10 +32,12 @@ export default class TagInput extends React.PureComponent { } render() { - const { className } = this.props; + const { className, isS } = this.props; return ( -
    +
    + {inputError} ); @@ -72,7 +76,7 @@ export default class TagInput extends React.PureComponent { if (temporaryHintText) { return ( - + {temporaryHintText} ); diff --git a/app/components/elements/postEditor/TagInput/index.scss b/app/components/elements/postEditor/TagInput/index.scss index 0e38600d8..8c995ef3c 100644 --- a/app/components/elements/postEditor/TagInput/index.scss +++ b/app/components/elements/postEditor/TagInput/index.scss @@ -4,6 +4,10 @@ position: relative; display: block; + &.small { + margin-left: 0.5rem; + } + &__input { width: 185px; height: $input-height; diff --git a/app/components/modules/CommentForm/CommentForm.jsx b/app/components/modules/CommentForm/CommentForm.jsx index 0b74ad349..1390f14ea 100644 --- a/app/components/modules/CommentForm/CommentForm.jsx +++ b/app/components/modules/CommentForm/CommentForm.jsx @@ -399,7 +399,6 @@ export default connect( progress(data); }, - useGolosImages: true, }, }); }, diff --git a/app/components/modules/Donate.jsx b/app/components/modules/Donate.jsx index 15991cebe..a4a5d25c6 100644 --- a/app/components/modules/Donate.jsx +++ b/app/components/modules/Donate.jsx @@ -18,6 +18,7 @@ import VoteSlider from 'app/components/elements/donate/VoteSlider' import { checkMemo } from 'app/utils/ParsersAndFormatters'; import { accuEmissionPerDay } from 'app/utils/StateFunctions' import { checkAllowed, AllowTypes } from 'app/utils/Allowance' +import { withScreenSize } from 'app/utils/ScreenSize' class Donate extends React.Component { constructor(props) { @@ -118,7 +119,8 @@ class Donate extends React.Component { } render() { - const { currentUser, currentAccount, opts, uias, sliderMax } = this.props + const { currentUser, currentAccount, opts, uias, sliderMax, + isS } = this.props const { sym } = opts const { isMemoEncrypted } = this.state @@ -139,35 +141,52 @@ class Donate extends React.Component { disabled = !isValid || (!values.sliderPercent && !values.amount.asset.amount) } + let bAmount =
    + +
    + let bPreset = + this.onPresetChange(amountStr, values, setFieldValue)} + /> + + return (
    this.onSliderChange(val, values, setFieldValue)} /> -
    + {isS ?
    +
    + {tt('transfer_jsx.donate_amount')} +
    +
    + {bAmount} +
    +
    :
    {tt('transfer_jsx.donate_amount')}
    -
    - -
    + {bAmount}
    - this.onPresetChange(amountStr, values, setFieldValue)} - /> - + {bPreset}
    -
    +
    } + {isS &&
    +
    + {bPreset} +
    +
    }
    @@ -175,10 +194,10 @@ class Donate extends React.Component {
    -
    +
    {tt('transfer_jsx.memo')}
    -
    +
      -
    • +
    • - {selected_sort_order && } + {selected_sort_order && hideOrders && }
    @@ -211,11 +224,14 @@ class Header extends React.Component {
    - {route.hideSubMenu ? null : -
    + {(route.hideSubMenu || hideOrders) ? null : +
    @@ -241,4 +257,4 @@ export default connect( account_meta: account_user, } } -)(Header); +)(withScreenSize(Header)) diff --git a/app/components/modules/Header.scss b/app/components/modules/Header.scss index 6d09266db..78fab296d 100644 --- a/app/components/modules/Header.scss +++ b/app/components/modules/Header.scss @@ -145,6 +145,7 @@ @media screen and (max-width: 39.9375em) { .shrink { padding: .3rem 1rem 0 1rem; + padding-left: 0px; } } } @@ -154,6 +155,12 @@ ul > li.Header__top-logo > a { transition: none; } +.Header__top-logo.small { + img { + width: 35px; + } +} + .Header__sub-nav { position: relative; padding: 0; @@ -170,7 +177,13 @@ ul > li.Header__top-logo > a { line-height: 1rem; padding: 1.2rem 0 1.2rem 0; margin: 0 0 0 2rem; - } + } + + @media screen and (max-width: 63.9375em) { + li { + margin: 0 0 0 1.3rem; + } + } // No margin adjustments for first child element in menu items li:first-child { margin: 0 0 0 0; @@ -242,8 +255,18 @@ ul > li.Header__top-logo > a { .Header__sort-order-menu { margin-left: 10px; + &:not(.me) > a { + font-size: 1.2em !important; + } + .VerticalMenu { left: 0; + + li { + a { + padding: 0.6rem 0.6rem; + } + } } } diff --git a/app/components/modules/LoginForm.jsx b/app/components/modules/LoginForm.jsx index 2ef21da2e..ddf7e99a3 100644 --- a/app/components/modules/LoginForm.jsx +++ b/app/components/modules/LoginForm.jsx @@ -13,6 +13,8 @@ import tt from 'counterpart'; import { APP_DOMAIN } from 'app/client_config'; import { translateError } from 'app/utils/ParsersAndFormatters'; import { authUrl, authRegisterUrl, } from 'app/utils/AuthApiClient'; +import LoginAppReminder from 'app/components/elements/app/LoginAppReminder' +import { openAppSettings } from 'app/components/pages/app/AppSettings' class LoginForm extends Component { @@ -155,7 +157,8 @@ class LoginForm extends Component { const authType = /^vote|comment/.test(opType) ? tt('loginform_jsx.posting') : tt('loginform_jsx.active_or_owner'); const submitLabel = loginBroadcastOperation ? tt('g.sign_in') : tt('g.login'); const cancelIsRegister = loginDefault && loginDefault.get('cancelIsRegister'); - let error = password.touched && password.error ? password.error : this.props.login_error; + let loginError = this.props.login_error + let error = password.touched && password.error ? password.error : (loginError && loginError.get('error')) if (error === 'account_frozen') { error = {tt('loginform_jsx.account_frozen')} @@ -179,6 +182,15 @@ class LoginForm extends Component {   {tt('loginform_jsx.you_may_use_this_active_key_on_other_more')} + } else if (error === 'Node failure') { + const NODE = loginError && loginError.get('node') + error = + {tt('app_settings.node_error_new_NODE', { NODE } )}  + {process.env.MOBILE_APP ? { + e.preventDefault() + openAppSettings() + }}>{tt('app_settings.node_error_new_NODE2')} : null} + } let message = null; if (msg) { @@ -255,6 +267,9 @@ class LoginForm extends Component {
    {form} + {(!process.env.MOBILE_APP && !process.env.DESKTOP_APP && !loginBroadcastOperation && !isMemo) &&
    + +
    }
    ) } diff --git a/app/components/modules/MiniTopics.jsx b/app/components/modules/MiniTopics.jsx new file mode 100644 index 000000000..72c1ee002 --- /dev/null +++ b/app/components/modules/MiniTopics.jsx @@ -0,0 +1,53 @@ +import React from 'react' +import {connect} from 'react-redux'; +import { withRouter } from 'react-router' + +import constants from 'app/redux/constants'; +import Topics from 'app/components/modules/Topics' + +class MiniTopics extends React.Component { + loadSelected = (keys) => { + let { router } = this.props + let { accountname, + category, + order = constants.DEFAULT_SORT_ORDER, + } = router.params + if (category === 'feed') { + accountname = order.slice(1); + order = 'by_feed'; + } + // if (isFetchingOrRecentlyUpdated(this.props.status, order, category)) return; + this.props.requestData({ order, keys, }); + }; + + render() { + let { loggedIn, categories, router } = this.props + let {category, order = constants.DEFAULT_SORT_ORDER} = router.params + if (category === 'feed') { + order = loggedIn ? 'created' : 'trending' + } + return
    + +
    + } +} + +module.exports = withRouter(connect( + (state) => { + return { + categories: state.global.get('tag_idx'), + loggedIn: !!state.user.get('current'), + }; + }, + (dispatch) => { + return { + requestData: (args) => dispatch({ type: 'REQUEST_DATA', payload: args, }), + }; + } +)(MiniTopics)) diff --git a/app/components/modules/MiniTopics.scss b/app/components/modules/MiniTopics.scss new file mode 100644 index 000000000..4897beead --- /dev/null +++ b/app/components/modules/MiniTopics.scss @@ -0,0 +1,10 @@ +.MiniTopics_in-header { + float: left; + margin-right: 1rem; + + > select { + border: none; + border-bottom: 1px solid $light-gray; + border-radius: 0; + } +} diff --git a/app/components/modules/Modals.jsx b/app/components/modules/Modals.jsx index d51bb37a4..d8d7f5d81 100644 --- a/app/components/modules/Modals.jsx +++ b/app/components/modules/Modals.jsx @@ -17,6 +17,7 @@ import AppDownload from 'app/components/modules/app/AppDownload' import user from 'app/redux/User'; import tr from 'app/redux/Transaction'; import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'; +import { withScreenSize } from 'app/utils/ScreenSize' let keyIndex = 0; @@ -75,6 +76,7 @@ class Modals extends React.Component { hideAppDownload, notifications, removeNotification, + isS, } = this.props; const notifications_array = notifications ? notifications.toArray().map(n => { @@ -87,6 +89,13 @@ class Modals extends React.Component { const loginClass = loginBlurring ? 'reveal-blurring' : undefined + let width400 + let width600 + if (!isS) { + width400 = '400px' + width600 = '600px' + } + return (
    {show_login_modal && @@ -96,11 +105,11 @@ class Modals extends React.Component { } - {show_donate_modal && + {show_donate_modal && } - {show_gift_nft_modal && + {show_gift_nft_modal && } @@ -108,7 +117,7 @@ class Modals extends React.Component { } - {show_change_account_modal && + {show_change_account_modal && } @@ -190,4 +199,4 @@ export default connect( removeNotification: (key) => dispatch({type: 'REMOVE_NOTIFICATION', payload: {key}}), }) -)(Modals) +)(withScreenSize(Modals)) diff --git a/app/components/modules/PostForm/PostForm.jsx b/app/components/modules/PostForm/PostForm.jsx index b6144b8a4..82dc38fb2 100644 --- a/app/components/modules/PostForm/PostForm.jsx +++ b/app/components/modules/PostForm/PostForm.jsx @@ -6,6 +6,7 @@ import Turndown from 'turndown'; import cn from 'classnames'; import tt from 'counterpart'; import { api } from 'golos-lib-js' +import { Asset } from 'golos-lib-js/lib/utils' import transaction from 'app/redux/Transaction'; import HtmlReady, { getTags } from 'shared/HtmlReady'; @@ -32,6 +33,8 @@ import { import { DRAFT_KEY, EDIT_KEY } from 'app/utils/postForm'; import { checkAllowed, AllowTypes } from 'app/utils/Allowance' import { makeOid, encryptPost, } from 'app/utils/sponsors' +import { withScreenSize } from 'app/utils/ScreenSize' +import { reloadLocation } from 'app/utils/app/RoutingUtils' const EDITORS_TYPES = { MARKDOWN: 1, @@ -96,6 +99,7 @@ class PostForm extends React.Component { postError: null, payoutType: PAYOUT_TYPES.PAY_100, curationPercent: DEFAULT_CURATION_PERCENT, + decryptFee: null, isPosting: false, uploadingCount: 0, }; @@ -149,6 +153,9 @@ class PostForm extends React.Component { state.tags = draft.tags; state.payoutType = draft.payoutType || PAYOUT_TYPES.PAY_50; state.curationPercent = draft.curationPercent || DEFAULT_CURATION_PERCENT; + if (draft.decryptFee) { + state.decryptFee = Asset(draft.decryptFee) + } if (state.editorId === EDITORS_TYPES.MARKDOWN_OLD) { state.editorId = EDITORS_TYPES.MARKDOWN; @@ -195,6 +202,7 @@ class PostForm extends React.Component { this.state.emptyBody = false; this.state.curationPercent = curationPercent; + this.state.decryptFee = editParams.decrypt_fee this.state.tags = tags; } @@ -204,7 +212,8 @@ class PostForm extends React.Component { } render() { - const { editMode, editParams, categories } = this.props; + const { editMode, editParams, categories, + isS, } = this.props; const { editorId, @@ -213,6 +222,7 @@ class PostForm extends React.Component { tags, payoutType, curationPercent, + decryptFee, isPreview, postError, uploadingCount, @@ -249,6 +259,7 @@ class PostForm extends React.Component { ]} activeId={editorId} onChange={this._onEditorChange} + isS={isS} /> {isPreview ? null : ( { + this.setState({ decryptFee: fee.clone() }, this._saveDraftLazy) + }; + _saveDraft = () => { const { editMode, editParams } = this.props; const { @@ -479,6 +497,7 @@ class PostForm extends React.Component { tags, payoutType, curationPercent, + decryptFee, } = this.state; try { @@ -498,6 +517,7 @@ class PostForm extends React.Component { tags, payoutType, curationPercent, + decryptFee: decryptFee ? decryptFee.toString() : null }; const json = JSON.stringify(save); @@ -535,7 +555,7 @@ class PostForm extends React.Component { }; } - _post = (visibleType) => { + _post = (visibleType, decryptFee) => { const { author, editMode } = this.props; const { title, tags, payoutType, curationPercent, editorId } = this.state; let error; @@ -636,6 +656,8 @@ class PostForm extends React.Component { data.permlink = editParams.permlink; data.parent_permlink = editParams.parent_permlink; data.__config.originalBody = editParams.body; + + data.__config.comment_options = {} } else { const commentOptions = { curator_rewards_percent: curationPercent, @@ -650,6 +672,10 @@ class PostForm extends React.Component { data.__config.comment_options = commentOptions; } + if (decryptFee) { + data.__config.comment_options.comment_decrypt_fee = decryptFee + } + this.setState({ isPosting: true, }); @@ -799,7 +825,12 @@ export default connect( }), dispatch => ({ async onPost(payload, editMode, visibleType, onSuccess, onError) { - if (visibleType === VISIBLE_TYPES.ONLY_SPONSORS) { + const onlySponsors = visibleType === VISIBLE_TYPES.ONLY_SPONSORS + + const decryptFee = payload.__config.comment_options.comment_decrypt_fee + const hasDecryptFee = (decryptFee && decryptFee.amount > 0) + + if (onlySponsors && !hasDecryptFee) { let pso try { pso = await api.getPaidSubscriptionOptionsAsync({ @@ -812,10 +843,12 @@ export default connect( return } if (!pso.author) { - window.location.href = '/@' + payload.author + '/sponsors' + reloadLocation('/@' + payload.author + '/sponsors') return } + } + if (onlySponsors || hasDecryptFee) { try { payload.body = await encryptPost(payload) } catch (err) { @@ -864,9 +897,8 @@ export default connect( progress(data); }, - useGolosImages: true, }, }); }, }) -)(PostForm); +)(withScreenSize(PostForm)) diff --git a/app/components/modules/PromotePost.jsx b/app/components/modules/PromotePost.jsx index f466ad81b..e1954e7b7 100644 --- a/app/components/modules/PromotePost.jsx +++ b/app/components/modules/PromotePost.jsx @@ -2,12 +2,14 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types' import {connect} from 'react-redux'; import ReactDOM from 'react-dom'; +import tt from 'counterpart'; + import transaction from 'app/redux/Transaction'; import LoadingIndicator from 'app/components/elements/LoadingIndicator'; import * as api from 'app/utils/APIWrapper' import { DEBT_TOKEN_SHORT, DEBT_TICKER} from 'app/client_config'; import { walletUrl } from 'app/utils/walletUtils' -import tt from 'counterpart'; +import { withScreenSize } from 'app/utils/ScreenSize' class PromotePost extends Component { @@ -95,7 +97,7 @@ class PromotePost extends Component { const DEBT_TOKEN = tt('token_names.DEBT_TOKEN') const {amount, loading, amountError, trxError, requiredAmount, alreadyInTop} = this.state; - const {currentAccount} = this.props; + const {currentAccount, isS} = this.props; const balanceValue = currentAccount.get('sbd_balance'); const balance = balanceValue ? balanceValue.split(' ')[0] : 0.0; const submitDisabled = !amount; @@ -112,7 +114,7 @@ class PromotePost extends Component {


    -
    +
    @@ -166,4 +168,4 @@ export default connect( })) } }) -)(PromotePost) +)(withScreenSize(PromotePost)) diff --git a/app/components/modules/Settings.jsx b/app/components/modules/Settings.jsx index 0b413e1db..2a0c6c680 100644 --- a/app/components/modules/Settings.jsx +++ b/app/components/modules/Settings.jsx @@ -1,9 +1,14 @@ import React from 'react'; import {connect} from 'react-redux' +import cookie from "react-cookie"; +import Dropzone from 'react-dropzone' +import ReactTooltip from 'react-tooltip' import user from 'app/redux/User'; import g from 'app/redux/GlobalReducer'; import tt from 'counterpart'; import throttle from 'lodash/throttle' +import {fromJS, Set, Map} from 'immutable' + import transaction from 'app/redux/Transaction' import { getMetadataReliably } from 'app/utils/NormalizeProfile'; import DoNotBother from 'app/components/elements/DoNotBother'; @@ -11,12 +16,11 @@ import Icon from 'app/components/elements/Icon'; import LoadingIndicator from 'app/components/elements/LoadingIndicator' import Userpic from 'app/components/elements/Userpic'; import reactForm from 'app/utils/ReactForm' -import {fromJS, Set, Map} from 'immutable' import UserList from 'app/components/elements/UserList'; import ContentSettings from 'app/components/elements/settings/ContentSettings' -import cookie from "react-cookie"; -import Dropzone from 'react-dropzone' +import AppSettings, { openAppSettings } from 'app/components/pages/app/AppSettings' import { LANGUAGES, DEFAULT_LANGUAGE, LOCALE_COOKIE_KEY, USER_GENDER } from 'app/client_config' +import { withScreenSize } from 'app/utils/ScreenSize' class Settings extends React.Component { @@ -84,6 +88,11 @@ class Settings extends React.Component { if (!notifyPresets) notifyPresets = { receive: true, donate: true, comment_reply: true, mention: true, message: true, fill_order: true, } + if (process.env.MOBILE_APP) { + if (notifyPresets.in_background === undefined) { + notifyPresets.in_background = true + } + } this.setState({notifyPresets}) } @@ -295,7 +304,7 @@ class Settings extends React.Component { const {profile_image, cover_image, name, about, gender, location, website, donatePresets, emissionDonatePct, notifyPresets, notifyPresetsTouched} = this.state - const {follow, block, account, isOwnAccount} = this.props + const {follow, block, account, isOwnAccount, isS} = this.props const following = follow && follow.getIn(['getFollowingAsync', account.name]); const ignores = isOwnAccount && block && block.getIn(['blocking', account.name, 'result']) const mutedInNew = isOwnAccount && props.mutedInNew; @@ -473,7 +482,7 @@ class Settings extends React.Component {


    -

    {tt('settings_jsx.notifications_settings')}

    +

    {isS ? tt('settings_jsx.notifications_settings2') : tt('settings_jsx.notifications_settings')}

    + {process.env.MOBILE_APP ? : null}
    } + {isOwnAccount && process.env.MOBILE_APP && +
    +
    +
    +

    {tt('settings_jsx.app_settings')}

    + {tt('settings_jsx.advanced_settings')} + +
    +
    } +
    } } @@ -565,7 +594,7 @@ export default connect( uploadImage: (file, progress) => { dispatch({ type: 'user/UPLOAD_IMAGE', - payload: {file, progress}, + payload: {file, progress, imageSizeLimit: 1000*1000}, }) }, changeLanguage: (language) => { @@ -588,4 +617,4 @@ export default connect( }); } }) -)(Settings) +)(withScreenSize(Settings)) diff --git a/app/components/modules/SponsorSubscription.jsx b/app/components/modules/SponsorSubscription.jsx index 22c7f0b14..2dd9a74f5 100644 --- a/app/components/modules/SponsorSubscription.jsx +++ b/app/components/modules/SponsorSubscription.jsx @@ -9,6 +9,7 @@ import AmountAssetField from 'app/components/elements/forms/AmountAssetField' import LoadingIndicator from 'app/components/elements/LoadingIndicator' import transaction from 'app/redux/Transaction' import session from 'app/utils/session' +import { withScreenSize } from 'app/utils/ScreenSize' class SponsorSubscription extends React.Component { constructor(props) { @@ -130,7 +131,7 @@ class SponsorSubscription extends React.Component { render() { const { pso, creating, submitting } = this.state - let { account, username } = this.props + let { account, username, isS } = this.props if (!username && process.env.BROWSER) { username = session.load().currentName } @@ -178,13 +179,14 @@ class SponsorSubscription extends React.Component { handleSubmit, isValid, values, dirty, setFieldValue, handleChange, }) => { const { symbol } = values.cost.asset + const clsColumn = 'column small-' + (isS ? '10' : '6') return (
    {tt('sponsors_jsx.cost')}
    -
    +
    @@ -195,7 +197,7 @@ class SponsorSubscription extends React.Component {
    - {pso.author ?
    + {pso.author ?
    @@ -205,7 +207,7 @@ class SponsorSubscription extends React.Component { {this._renderSubmittingIndicator()} {this.state.saved ? {tt('g.saved') + '!'} : null} {this.state.created ? {tt('sponsors_jsx.created') + '!'} : null} -
    :
    +
    :
    @@ -312,4 +314,4 @@ export default connect( dispatch({type: 'FETCH_STATE', payload: {pathname}}) } }) -)(SponsorSubscription) +)(withScreenSize(SponsorSubscription)) diff --git a/app/components/modules/TopRightMenu.jsx b/app/components/modules/TopRightMenu.jsx index 8b6af8334..6cb531d76 100644 --- a/app/components/modules/TopRightMenu.jsx +++ b/app/components/modules/TopRightMenu.jsx @@ -15,6 +15,7 @@ import LoadingIndicator from 'app/components/elements/LoadingIndicator'; import NotifiCounter from 'app/components/elements/NotifiCounter'; import { LIQUID_TICKER, DEBT_TICKER } from 'app/client_config'; import LocalizedCurrency from 'app/components/elements/LocalizedCurrency'; +import { openAppSettings } from 'app/components/pages/app/AppSettings' import { vestsToSteem, toAsset } from 'app/utils/StateFunctions'; import { authRegisterUrl, } from 'app/utils/AuthApiClient'; import { msgsHost, msgsLink, } from 'app/utils/ExtLinkUtils'; @@ -55,24 +56,27 @@ const calculateEstimateOutput = ({ account, price_per_golos, savings_withdraws, return Number(((total_steem * price_per_golos) + total_sbd).toFixed(2) ); } -function TopRightMenu({account, savings_withdraws, price_per_golos, globalprops, username, showLogin, goChangeAccount, loggedIn, vertical, navigate, probablyLoggedIn, location, locationQueryParams, toggleNightmode}) { +function TopRightMenu({account, savings_withdraws, price_per_golos, globalprops, username, showLogin, goChangeAccount, loggedIn, vertical, navigate, probablyLoggedIn, location, locationQueryParams, toggleNightmode, + hideOrders, hideOrdersMe, }) { + if (loggedIn) { + hideOrders = hideOrdersMe + } const APP_NAME = tt('g.APP_NAME'); const mcn = 'menu' + (vertical ? ' vertical show-for-small-only' : ''); const mcl = vertical ? '' : ' sub-menu'; const lcn = vertical ? '' : 'show-for-large'; - const scn = vertical ? '' : 'show-for-medium'; const nav = navigate || defaultNavigate; const topbutton =
  • {tt('g.topbutton')}
  • ; - const submitStory =
  • + const submitStory = (vertical || !hideOrders) &&
  • {tt('g.submit_a_story')}
  • ; - const submitStoryPencil =
  • + const submitStoryPencil = hideOrders &&
  • @@ -88,13 +92,13 @@ function TopRightMenu({account, savings_withdraws, price_per_golos, globalprops, const messagesLink = msgsHost() ? msgsLink() : ''; const ordersLink = walletUrl(`/@${username}/filled-orders`) - const faqItem =
  • + const faqItem = (vertical || !hideOrders) &&
  • ; - const searchItem =
  • + const searchItem = (vertical || !hideOrders) &&
  • @@ -112,15 +116,15 @@ function TopRightMenu({account, savings_withdraws, price_per_golos, globalprops, const registerUrl = authRegisterUrl() + (invite ? ('?invite=' + invite) : ''); const additional_menu = [] - if (!loggedIn) { + if (!loggedIn && hideOrders) { additional_menu.push( - { link: '/login.html', onClick: showLogin, value: tt('g.login'), className: 'show-for-small-only' }, + { link: '/login.html', onClick: showLogin, value: tt('g.login') }, { link: registerUrl, onClick: (e) => { e.preventDefault(); window.location.href = registerUrl; }, - value: tt('g.sign_up'), className: 'show-for-small-only' } + value: tt('g.sign_up') } ) } additional_menu.push( @@ -134,6 +138,15 @@ function TopRightMenu({account, savings_withdraws, price_per_golos, globalprops, { link: 'https://wiki.golos.id/', icon: 'new/wikipedia', value: tt("navigation.wiki"), target: 'blank' }, { link: 'https://explorer.golos.id/', icon: 'cog', value: tt("navigation.explorer"), target: 'blank' } ); + if (!loggedIn && process.env.MOBILE_APP) { + const openSettings = (e) => { + e.preventDefault() + openAppSettings() + } + additional_menu.push( + { link: '#', onClick: openSettings, icon: 'new/setting', value: tt("g.settings"), } + ) + } const navAdditional = }, {link: discussionsLink, icon: 'new/bell', value: tt('g.discussions'), addon: }, {link: mentionsLink, icon: 'new/mention', value: tt('g.mentions'), addon: }, - {link: walletLink, target: walletTarget(), icon: 'new/wallet', value: tt('g.wallet'), addon: }, + {link: walletLink, target: walletTarget(), icon: 'new/wallet', value: tt('g.wallet'), addon: }, {link: donatesLink, target: walletTarget(), icon: 'hf/hf8', value: tt('g.rewards'), addon: }, (messagesLink ? {link: messagesLink, icon: 'new/envelope', value: tt('g.messages'), target: '_blank', addon: } : @@ -172,14 +185,14 @@ function TopRightMenu({account, savings_withdraws, price_per_golos, globalprops, return (
      - + {(vertical || !hideOrders) && } {faqItem} {searchItem} -
    • + {!hideOrders &&
    • } {topbutton} {submitStory} {!vertical && submitStoryPencil} -
    • + {!hideOrders &&
    • }
    -
    +
  • } {navAdditional} @@ -213,10 +226,10 @@ function TopRightMenu({account, savings_withdraws, price_per_golos, globalprops, {faqItem} {searchItem}
  • - {!probablyLoggedIn &&
  • + {!probablyLoggedIn && (vertical || !hideOrders) &&
  • {tt('g.login')}
  • } - {!probablyLoggedIn &&
  • + {!probablyLoggedIn && (vertical || !hideOrders) &&
  • {tt('g.sign_up')}
  • } {probablyLoggedIn &&
  • diff --git a/app/components/modules/TopRightMenu.scss b/app/components/modules/TopRightMenu.scss index 10da325c6..8604337cd 100644 --- a/app/components/modules/TopRightMenu.scss +++ b/app/components/modules/TopRightMenu.scss @@ -6,6 +6,16 @@ margin: 0 0 0 1rem; } } +@media screen and (max-width: 39.9375em) { + .sub-menu { + li { + margin: 0 5px; + } + li:last-child { + margin: 0 0 0 0.5rem; + } + } +} li.Header__profile { padding: 0; diff --git a/app/components/pages/Topics.jsx b/app/components/modules/Topics.jsx similarity index 89% rename from app/components/pages/Topics.jsx rename to app/components/modules/Topics.jsx index 6ee76ccf1..1c4888aad 100644 --- a/app/components/pages/Topics.jsx +++ b/app/components/modules/Topics.jsx @@ -2,11 +2,13 @@ import React from 'react'; import PropTypes from 'prop-types' import { Link } from 'react-router'; import { browserHistory } from 'react-router'; +import cookie from "react-cookie"; import tt from 'counterpart'; +import capitalize from 'lodash/capitalize' + import { detransliterate } from 'app/utils/ParsersAndFormatters'; import { isCyrillicTag, processCyrillicTag } from 'app/utils/tags'; import { SELECT_TAGS_KEY } from 'app/client_config'; -import cookie from "react-cookie"; export default class Topics extends React.Component { static propTypes = { @@ -119,10 +121,22 @@ export default class Topics extends React.Component { let isSelected = false if (compact) { - return { + const { value } = e.target + if (value === '_home') { + e.preventDefault() + browserHistory.push(homePath) + return + } + browserHistory.push(value) + }} value={currentValue}> + + {(process.env.BROWSER && location.pathname !== homePath) ? + : null} {categories.map(cat => { const link = order ? `/${order}/${cat}` : `/hot/${cat}`; + cat = capitalize(cat) return })} ; diff --git a/app/components/pages/Topics.scss b/app/components/modules/Topics.scss similarity index 100% rename from app/components/pages/Topics.scss rename to app/components/modules/Topics.scss diff --git a/app/components/modules/app/AppDownload.jsx b/app/components/modules/app/AppDownload.jsx index 850c3edae..0dd73faf8 100644 --- a/app/components/modules/app/AppDownload.jsx +++ b/app/components/modules/app/AppDownload.jsx @@ -1,14 +1,30 @@ import React from 'react' import tt from 'counterpart' +import Icon from '@elements/Icon' +import QRCode from 'app/components/elements/QrCode' + +const updaterHost = 'https://files.golos.app' +const winUrl = new URL('/api/exe/desktop/windows/latest', updaterHost) +const linuxUrl = new URL('/api/exe/desktop/linux/latest', updaterHost) +const androidUrl = new URL('/api/exe/blogs/android/latest', updaterHost) + class AppDownload extends React.Component { + state = { + qrShow: false, + } + componentDidMount() { } + showQR = (e) => { + e.preventDefault() + this.setState({ + qrShow: !this.state.qrShow, + }) + } + render() { - const updaterHost = 'https://files.golos.app' - const winUrl = new URL('/api/exe/desktop/windows/latest', updaterHost) - const linuxUrl = new URL('/api/exe/desktop/linux/latest', updaterHost) return } diff --git a/app/components/pages/PostsIndex.jsx b/app/components/pages/PostsIndex.jsx index b0ce07f92..0bea7aaf7 100644 --- a/app/components/pages/PostsIndex.jsx +++ b/app/components/pages/PostsIndex.jsx @@ -2,21 +2,24 @@ import React from 'react'; import PropTypes from 'prop-types' import {connect} from 'react-redux'; -import Topics from './Topics'; +import {Link} from 'react-router'; +import tt from 'counterpart'; +import Immutable from "immutable"; +import cookie from "react-cookie"; +import cn from 'classnames' + import constants from 'app/redux/constants'; import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'; import PostsList from 'app/components/cards/PostsList'; import {isFetchingOrRecentlyUpdated} from 'app/utils/StateFunctions'; -import {Link} from 'react-router'; import MarkNotificationRead from 'app/components/elements/MarkNotificationRead'; -import tt from 'counterpart'; -import Immutable from "immutable"; import Callout from 'app/components/elements/Callout' import CMCWidget from 'app/components/elements/market/CMCWidget' +import Topics from 'app/components/modules/Topics' import { APP_NAME, SELECT_TAGS_KEY } from 'app/client_config'; -import cookie from "react-cookie"; import transaction from 'app/redux/Transaction' import { getMetadataReliably } from 'app/utils/NormalizeProfile'; +import { withScreenSize } from 'app/utils/ScreenSize' class PostsIndex extends React.Component { @@ -123,7 +126,7 @@ class PostsIndex extends React.Component { } render() { - let { loggedIn, categories, has_from_search } = this.props; + let { loggedIn, categories, has_from_search, hideOrders, hideOrdersMe, } = this.props; let {category, order = constants.DEFAULT_SORT_ORDER} = this.props.routeParams; let topics_order = order; let posts = []; @@ -172,10 +175,14 @@ class PostsIndex extends React.Component { posts = posts.slice(slice_step) } + if (loggedIn) hideOrders = hideOrdersMe + return (
    -
    -
    +
    + {hideOrders &&
    -
    +
    } { markNotificationRead } {(promo_posts && promo_posts.size) ?
    select { border: none; border-bottom: 1px solid $medium-gray; @@ -68,17 +72,9 @@ color: gray; } -/* Small only */ -@media screen and (max-width: 39.9375em) { - .PostsIndex__left { - padding: 0; - } - .PostsIndex__topics_compact { - padding: 0 0.5rem; - float: none; - width: auto; - margin-top: 1.5rem; - } +/* hideOrders */ +.PostsIndex__left.ho { + padding: 0; } /* Medium and up */ diff --git a/app/components/pages/Search.jsx b/app/components/pages/Search.jsx index 6826a754a..8bf6b1bc6 100644 --- a/app/components/pages/Search.jsx +++ b/app/components/pages/Search.jsx @@ -3,11 +3,7 @@ import golos from 'golos-lib-js'; import { Link } from 'react-router'; import { browserHistory } from 'react-router'; import tt from 'counterpart'; -import Icon from 'app/components/elements/Icon'; -import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper'; -import remarkableStripper from 'app/utils/RemarkableStripper'; import sanitize from 'sanitize-html'; -import { detransliterate } from 'app/utils/ParsersAndFormatters'; import truncate from 'lodash/truncate'; import Pagination from 'rc-pagination'; import localeEn from 'rc-pagination/lib/locale/en_US'; @@ -16,6 +12,13 @@ if (typeof(document) !== 'undefined') require('rc-pagination/assets/index.css'); let Multiselect; if (typeof(document) !== 'undefined') Multiselect = require('multiselect-react-dropdown').Multiselect; +import Icon from 'app/components/elements/Icon'; +import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper'; +import remarkableStripper from 'app/utils/RemarkableStripper'; +import { detransliterate } from 'app/utils/ParsersAndFormatters'; +import { hrefClick } from 'app/utils/app/RoutingUtils' +import { withScreenSize } from 'app/utils/ScreenSize' + class Search extends React.Component { constructor(props) { super(props); @@ -273,7 +276,95 @@ class Search extends React.Component { }); }; + _renderWhereButton = () => { + const { isS } = this.props + + let button = + +    + + + + if (isS) { + return
    + {button} +
    + } + + return button + } + + _renderSettings = () => { + const { isS } = this.props + + const dateFrom = + const dateSep =  —  + const dateTo = + const sep =    + const dateAll = (this.state.dateFrom || this.state.dateTo) && + + + + const selAuthor = Multiselect && + const selTags = Multiselect && + let sel = + {selAuthor} + {sep} + {isS ? sep : null} + {selTags} + + if (isS) { + sel =
    + {sel} +
    + } + + return
    + {dateFrom} + {dateSep} + {dateTo} + {isS ? null : sep} + {dateAll} + {isS ? null : sep} + {sel} +
    + } + render() { + const { isS } = this.props + let wordWrap + if (isS) { + wordWrap = 'break-word' + } let results = []; let totalPosts = 0; let display = null; @@ -307,13 +398,13 @@ class Search extends React.Component { body = sanitize(body, {allowedTags: ['em', 'img']}); return (
    -
    +
     — @ {hit.fields.author[0]} -
    +

    ); }); @@ -343,56 +434,14 @@ class Search extends React.Component { />
    ); } + return (
    - + {isS ? null : }
    - -    - -
    -
    - -  —  - -    - {(this.state.dateFrom || this.state.dateTo) ? - - : null} -    - {Multiselect ? : null} -    - {Multiselect ? : null} + {this._renderWhereButton()}
    + {this._renderSettings()} {display}

    @@ -410,5 +459,5 @@ class Search extends React.Component { module.exports = { path: '/search(/:query)', - component: Search + component: withScreenSize(Search) }; diff --git a/app/components/pages/Search.scss b/app/components/pages/Search.scss index b42b95662..2be46fbc1 100644 --- a/app/components/pages/Search.scss +++ b/app/components/pages/Search.scss @@ -72,6 +72,9 @@ .esearch-settings input { width: 150px; } + .esearch-settings input.searchBox { + width: 140px; + } .another-search { font-size: 1.3em; } diff --git a/app/components/pages/UserProfile.jsx b/app/components/pages/UserProfile.jsx index 19bc741ef..0e0191218 100644 --- a/app/components/pages/UserProfile.jsx +++ b/app/components/pages/UserProfile.jsx @@ -38,6 +38,7 @@ import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper'; import Userpic from 'app/components/elements/Userpic'; import Callout from 'app/components/elements/Callout'; import normalizeProfile, { getLastSeen } from 'app/utils/NormalizeProfile'; +import { withScreenSize } from 'app/utils/ScreenSize' export default class UserProfile extends React.Component { constructor(props) { @@ -79,7 +80,11 @@ export default class UserProfile extends React.Component { np.location.pathname !== this.props.location.pathname || np.routeParams.accountname !== this.props.routeParams.accountname || np.follow_count !== this.props.follow_count || - ns.repLoading !== this.state.repLoading + ns.repLoading !== this.state.repLoading || + np.hideMainMe !== this.props.hideMainMe || + np.hideMainFor !== this.props.hideMainFor || + np.hideRewardsMe !== this.props.hideRewardsMe || + np.hideRewardsFor !== this.props.hideRewardsFor ) } @@ -153,8 +158,9 @@ export default class UserProfile extends React.Component { render() { const { - props: {current_user, current_account, wifShown, global_status, follow}, - onPrint + props: {current_user, current_account, wifShown, global_status, follow, + hideMainMe, hideRewardsMe, hideMainFor, hideRewardsFor,}, + onPrint, } = this; let { accountname, section, id, action } = this.props.routeParams; // normalize account from cased params @@ -428,6 +434,56 @@ export default class UserProfile extends React.Component { accountjoin = transferFromSteemToGolosDate; } + const hideMain = isMyAccount ? hideMainMe : hideMainFor + const hideRewards = isMyAccount ? hideRewardsMe : hideRewardsFor + let mentionCounter = isMyAccount && + let disCounter = isMyAccount && + let walletCounter = isMyAccount && + + let kebab + let kebabNotify = '' + if (hideMain) { + let kebabMenu = [ + { link: `/@${accountname}/mentions`, label: tt('g.mentions'), value: tt('g.mentions'), addon: mentionCounter } + ] + if (isMyAccount) { + kebabMenu.unshift({ link: `/@${accountname}/discussions`, label: tt('g.discussions'), value: tt('g.discussions'), addon: disCounter }) + kebabMenu.push({ value: '-' }) + kebabMenu.push({ link: `/@${accountname}/settings`, label: tt('g.settings'), value: tt('g.settings') }) + kebabNotify += ',mention,subscriptions' + } + if (hideRewards) { + kebabMenu = [ + ...rewardsMenu, + { value: '-' }, + ...kebabMenu, + ] + kebabNotify += ',donate,donate_msgs' + } + kebabMenu = [ + { link: walletUrl(`/@${accountname}/transfers`), target: walletTarget(), label: tt('g.wallet'), value: tt('g.wallet'), addon: walletCounter }, + { value: '-' }, + ...kebabMenu, + ] + kebabNotify += ',send,receive,fill_order,nft_receive,nft_token_sold,nft_buy_offer' + + if (kebabMenu.length) { + if (kebabMenu[kebabMenu.length - 1].value === '-') kebabMenu.pop() + } + if (kebabNotify[0] === ',') kebabNotify = kebabNotify.slice(1) + kebab = kebabMenu.length ? } + > + + + {(isMyAccount && kebabNotify) ? : null} + + : null + } + const top_menu =
    @@ -440,18 +496,18 @@ export default class UserProfile extends React.Component { {tt('g.replies')} {isMyAccount && } - {isMyAccount ? - {tt('g.discussions')} + {(!hideMain && isMyAccount) ? + {tt('g.discussions')} {disCounter} : null} - - {tt('g.mentions')} {isMyAccount && } - + {!hideMain && + {tt('g.mentions')} {mentionCounter} + }
    - - {tt('g.wallet')} {isMyAccount && } - - + {tt('g.wallet')} {walletCounter} + } + {!hideRewards && } - + } {isMyAccount && msgsHost() ? : null} - {isMyAccount ? + {(isMyAccount && !hideMain) ? : null} + {kebab}
    @@ -641,5 +698,5 @@ module.exports = { }, requestData: (args) => dispatch({type: 'REQUEST_DATA', payload: args}), }) - )(UserProfile) + )(withScreenSize(UserProfile)) }; diff --git a/app/components/pages/UserProfile.scss b/app/components/pages/UserProfile.scss index 380b90232..3277e27d5 100644 --- a/app/components/pages/UserProfile.scss +++ b/app/components/pages/UserProfile.scss @@ -306,7 +306,7 @@ } } -@media screen and (max-width: 400px) { +@media screen and (max-width: 330px) { .UserProfile { &__filler { display: none; diff --git a/app/components/pages/app/AppSettings.jsx b/app/components/pages/app/AppSettings.jsx index cb6bd2f25..a41bf3e83 100644 --- a/app/components/pages/app/AppSettings.jsx +++ b/app/components/pages/app/AppSettings.jsx @@ -2,6 +2,8 @@ import React from 'react' import tt from 'counterpart' import { Formik, Field } from 'formik' +import Icon from 'app/components/elements/Icon' + class AppSettings extends React.Component { _onSubmit = (data) => { let cfg = { ...$STM_Config } @@ -22,12 +24,25 @@ class AppSettings extends React.Component { cfg.images.use_img_proxy = data.use_img_proxy cfg.auth_service.host = data.auth_service cfg.notify_service.host = data.notify_service + cfg.notify_service.host_ws = data.notify_service_ws cfg.elastic_search.url = data.elastic_search - cfg.main_app = data.main_app + if (process.env.MOBILE_APP) { + cfg = JSON.stringify(cfg) + localStorage.setItem('app_settings', cfg) + window.location.href = '/' + return + } else { + cfg.main_app = data.main_app + } window.appSettings.save(cfg) } - _onClose = () => { + _onClose = (e) => { + e.preventDefault() + if (process.env.MOBILE_APP) { + window.location.href = '/' + return + } window.close() } @@ -38,6 +53,7 @@ class AppSettings extends React.Component { use_img_proxy: $STM_Config.images.use_img_proxy, auth_service: $STM_Config.auth_service.host, notify_service: $STM_Config.notify_service.host, + notify_service_ws: $STM_Config.notify_service.host_ws || '', elastic_search: $STM_Config.elastic_search.url, main_app: $STM_Config.main_app, } @@ -49,6 +65,17 @@ class AppSettings extends React.Component { this.makeInitialValues() } + showLogs = (e) => { + e.preventDefault() + NativeLogs.getLog( + 200, + false, + logs => { + alert(logs) + } + ) + } + _renderNodes(ws_connection_client) { let fields = [] for (let i in $STM_Config.ws_connection_app) { @@ -85,8 +112,14 @@ class AppSettings extends React.Component { } render() { + const { MOBILE_APP } = process.env return
    -

    {tt('g.settings')}

    +

    + {MOBILE_APP ? + + : null} + {MOBILE_APP ? tt('app_settings.mobile_title') : tt('g.settings')} +

    {tt('app_settings.to_save_click_button')}
    @@ -150,6 +183,17 @@ class AppSettings extends React.Component {
    +
    +
    + {tt('app_settings.notify_service_ws')} +
    + +
    +
    +
    {tt('app_settings.elastic_search')} @@ -161,7 +205,7 @@ class AppSettings extends React.Component {
    -
    + {MOBILE_APP ? null :
    {tt('app_settings.main_app')}
    @@ -174,16 +218,19 @@ class AppSettings extends React.Component {
    -
    +
    }
    - + } + {MOBILE_APP ? + {tt('app_settings.logs')} + : null}
    @@ -197,3 +244,15 @@ module.exports = { path: '/__app_settings', component: AppSettings, } + +module.exports.openAppSettings = function() { + if (!process.env.MOBILE_APP) { + window.location.href = '/__app_settings' + return + } + const { pathname } = window.location + window.location.href = '/#app-settings' + if (pathname === '/' && window.appMounted) { + window.location.reload() + } +} diff --git a/app/locales/en.json b/app/locales/en.json index dcbbd8ecf..9196646dc 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -69,6 +69,7 @@ "age": "age", "approximately": "approximately", "all_langs": "All languages", + "all_posts": "All posts", "amount": "Amount", "and": "and", "at_least": "at least", @@ -214,7 +215,7 @@ "tag": "Tag", "team": "Team", "to": " to ", - "topics": "Topics", + "topics": "Categories", "transaction_history": "Transaction History", "transfer": "Transfer ", "transfer2": "Transfer", @@ -306,7 +307,7 @@ "show_more_low_value_posts": "Show more low value posts", "select_topic": "Select Topic", "tags_filter": "Filter", - "no_tags_selected": "No tags selected", + "no_tags_selected": "no categories selected", "filter": "Filter", "show_more_topics": "Go to tag rating", "personal_info_will_be_private": "Your personal information will be kept", @@ -466,6 +467,7 @@ "tag_your_story": "Tag (up to 5 tags), the first tag is your main category.", "select_a_tag": "Select tag", "select_a_category": "Select category", + "select_a_category2": "Category", "maximum_tag_length_is_24_characters": "Maximum tag length is 24 characters", "use_only_lowercase_letters": "Use only lowercase letters", "use_one_dash": "Use only one dash", @@ -859,12 +861,18 @@ "choose_preset_tips": "Choose the amounts for the reward buttons", "emission_donate_pct": "Donation percent of your daily token emission", "notifications_settings": "Notification settings", + "notifications_settings2": "Notification settings", "notifications_transfer": "Transfer notifications", "notifications_donate": "Donate notifications", "notifications_reply": "Reply notifications", "notifications_mention": "Mention notifications", "notifications_message": "Private message notifications", "notifications_order": "Market order filled", + "notifications_bg": "Show notifications in background", + "notifications_bg_title": "Show notification when app hidden, screen is off", + "app_settings": "App Settings", + "advanced_settings": "Select the blockchain node, and another advanced settings...", + "open": "Open", "invalid_url": "Invalid URL", "name_is_too_long": "Name is too long", "name_must_not_begin_with": "Name must not begin with @", @@ -966,7 +974,9 @@ "login_first": "Please logged in first ...", "login_with_posting_key": "Login with your posting key", "server_unavailable": "The server is unavailable", - "image_size_is_too_large": "Upload images up to 1 MB in size" + "image_size_is_too_large": "Upload images up to 1 MB in size", + "low_reputation": "Too low reputation. Need at least ", + "low_golos_power_NEED_ADD": "No Golos Power. Need %(NEED)s, add please %(ADD)s." } } }, @@ -1154,21 +1164,30 @@ "mention_comment": " mentioned you in their comment", "message": " sent you a private message" }, + "login_app_reminder": { + "or_download_for": "...or download for ", + "title": "Click to download for " + }, "app_download": { - "title": "Download Golos Desktop" + "title": "Download Golos Desktop", + "download_for": "Download for", + "mobile": "Download Golos Blogs for" }, "app_reminder": { - "text": "Install desktop application for Windows or Linux to receive data directly from blockchain" + "text": "Install desktop application for Windows, Linux, Android", + "text2": "to receive data directly from blockchain" }, "app_goto_url": { "goto": "Goto", "wrong_domain_DOMAINS": "This address is not belong to GOLOS Blogs.\nGOLOS Blogs domains are: %(DOMAINS)s\nThis address will be opened in external browser." }, "app_settings": { + "mobile_title": "Settings of Blogs", "ws_connection_client": "GOLOS node URL", "img_proxy_prefix": "Use Proxy For Images", "auth_service": "Golos Auth & Registration Service (for notifications)", "notify_service": "Golos Notify Service (for notifications)", + "notify_service_ws": "Golos Notify Service WebSocket (for notifications)", "messenger_service": "Messenger", "elastic_search": "Search", "main_app": "When start app, open:", @@ -1176,8 +1195,11 @@ "wallet_service": "Wallet", "save_and_restart": "Save and restart", "cancel": "Cancel", - "node_error_NODE": "Failed to connect to node %(NODE)s. Perhaps it's a problem with the Internet. Or try setting another GOLOS node in the Settings menu.", - "to_save_click_button": "To save settings, click \"Save and restart\" button at bottom of the window." + "node_error_NODE": "Не удалось подключиться к ноде %(NODE)s. Возможно, это проблемы с интернетом. Или попробуйте задать другую ноду GOLOS в меню Настройки.", + "node_error_new_NODE": "Не удалось подключиться к ноде %(NODE)s. Возможно, это проблемы с интернетом (или с нодой).", + "node_error_new_NODE2": "Выбрать другую ноду", + "to_save_click_button": "To save settings, click \"Save and restart\" button at bottom of the window.", + "logs": "Logs" }, "app_update": { "notify_VERSION": "New version is available Golos Desktop - %(VERSION)s", diff --git a/app/locales/ru-RU.json b/app/locales/ru-RU.json index 9a6a8d85b..4af19b0ef 100644 --- a/app/locales/ru-RU.json +++ b/app/locales/ru-RU.json @@ -15,6 +15,7 @@ "tag_your_story": "Добавьте тэги, первый станет категорией.", "select_a_tag": "Выбрать тэг", "select_a_category": "Выберите категорию", + "select_a_category2": "Категория", "maximum_tag_length_is_24_characters": "Максимальная длина категории 24 знака", "use_only_lowercase_letters": "Используйте только символы нижнего регистра", "use_one_dash": "Используйте только одно тире", @@ -129,6 +130,7 @@ "active": "Активный ключ", "advanced": "Расширенные опции", "all_langs": "Показать всё", + "all_posts": "Все посты", "amount": "Количество", "and": "и", "are_you_sure": "Вы уверены?", @@ -277,7 +279,7 @@ "tag": "Тэг", "team": "Команда", "to": " к ", - "topics": "Темы", + "topics": "Категории", "transaction_history": "История транзакций", "transfer": "Передать ", "transfer2": "Перевод", @@ -721,12 +723,18 @@ "choose_preset_tips": "Выберите суммы для кнопок вознаграждений", "emission_donate_pct": "Процент от вашей суточной эмиссии токенов для вознаграждения", "notifications_settings": "Настройки уведомлений на сайте", + "notifications_settings2": "Настройки уведомлений", "notifications_transfer": "Перевод средств", "notifications_donate": "Вознаграждения (донаты)", "notifications_reply": "Ответ на комментарий или пост", "notifications_mention": "Упоминание @ника в посте или комментарии", "notifications_message": "Новое сообщение", "notifications_order": "Сработал ордер на бирже", + "notifications_bg": "Показывать уведомления в фоне", + "notifications_bg_title": "Показывать ли уведомления при свернутом приложении, выключенном экране", + "app_settings": "Настройки приложения", + "advanced_settings": "Выбор ноды и другие расширенные настройки...", + "open": "Открыть", "invalid_url": "Недопустимый URL-адрес", "invalid_ws": "wss://api-full.golos.id/ws", "invalid_ip_port": "Пример: 120.16.10.18:2001", @@ -1029,7 +1037,9 @@ "login_first": "Пожалуйста, выполните вход в систему ...", "login_with_posting_key": "Недоступен постинг ключ", "server_unavailable": "Ошибка доступа к серверу", - "image_size_is_too_large": "Загрузите фото размером до 1 МБ" + "image_size_is_too_large": "Загрузите фото размером до 1 МБ", + "low_reputation": "Не хватает репутации. Нужно не менее ", + "low_golos_power_NEED_ADD": "Не хватает Силы Голоса. Нужно %(NEED)s, добавьте еще %(ADD)s." } } }, @@ -1220,22 +1230,30 @@ "mention_comment": " упомянул вас в комментарии", "message": " написал вам сообщение" }, + "login_app_reminder": { + "or_download_for": "...или скачайте для ", + "title": "Нажмите, чтобы скачать для " + }, "app_download": { "title": "Скачать Golos Desktop", - "download_for": "Скачать для" + "download_for": "Скачать для", + "mobile": "Скачать Golos Блоги для" }, "app_reminder": { - "text": "Установите десктоп-приложение для Windows или Linux и получайте информацию напрямую с блокчейна" + "text": "Установите приложение для Windows, Linux, Android", + "text2": "и получайте информацию напрямую с блокчейна" }, "app_goto_url": { "goto": "Перейти", "wrong_domain_DOMAINS": "Похоже, эта ссылка не с GOLOS Блогов.\nДомены GOLOS Блогов: %(DOMAINS)s\nЭта ссылка будет открыта во внешнем браузере." }, "app_settings": { + "mobile_title": "Настройки Блогов", "ws_connection_client": "Адрес ноды GOLOS", "img_proxy_prefix": "Использовать прокси для изображений", "auth_service": "Golos Auth & Registration Service (для уведомлений)", "notify_service": "Golos Notify Service (для уведомлений)", + "notify_service_ws": "Golos Notify Service WebSocket (для уведомлений)", "messenger_service": "Мессенджер", "elastic_search": "Поиск", "main_app": "При запуске открывать:", @@ -1244,7 +1262,10 @@ "save_and_restart": "Сохранить и перезапустить", "cancel": "Отмена", "node_error_NODE": "Не удалось подключиться к ноде %(NODE)s. Возможно, это проблемы с интернетом. Или попробуйте задать другую ноду GOLOS в меню Настройки.", - "to_save_click_button": "Чтобы сохранить настройки, нажмите кнопку \"Сохранить и перезапустить\" внизу окна." + "node_error_new_NODE": "Не удалось подключиться к ноде %(NODE)s. Возможно, это проблемы с интернетом (или с нодой).", + "node_error_new_NODE2": "Выбрать другую ноду", + "to_save_click_button": "Чтобы сохранить настройки, нажмите кнопку \"Сохранить и перезапустить\" внизу окна.", + "logs": "Логи" }, "app_update": { "notify_VERSION": "Доступна новая версия Golos Desktop - %(VERSION)s", diff --git a/app/redux/TransactionSaga.js b/app/redux/TransactionSaga.js index b28fcddb3..def2644e8 100644 --- a/app/redux/TransactionSaga.js +++ b/app/redux/TransactionSaga.js @@ -495,6 +495,7 @@ function* preBroadcast_comment({operation, username}) { allow_votes = true, allow_curation_rewards = true, curator_rewards_percent = null, + comment_decrypt_fee } = comment_options const extensions = []; @@ -508,6 +509,10 @@ function* preBroadcast_comment({operation, username}) { extensions.push([ 2, { percent: curator_rewards_percent }]) } + if (comment_decrypt_fee) { + extensions.push([ 3, { fee: comment_decrypt_fee.toString() }]) + } + comment_op.push( ['comment_options', { author, diff --git a/app/redux/User.js b/app/redux/User.js index d39cb1f09..a25e92497 100644 --- a/app/redux/User.js +++ b/app/redux/User.js @@ -133,7 +133,8 @@ export default createModule({ }, { action: 'LOGIN_ERROR', - reducer: (state, {payload: {error}}) => state.merge({ login_error: error, logged_out: undefined }) + reducer: (state, {payload: {error, ...rest}}) => state.merge({ login_error: { error, ...rest }, + logged_out: undefined }) }, { action: 'LOGOUT', diff --git a/app/redux/UserSaga.js b/app/redux/UserSaga.js index 870c10b7c..ab7ab6e38 100644 --- a/app/redux/UserSaga.js +++ b/app/redux/UserSaga.js @@ -12,7 +12,7 @@ import { fetchState } from 'app/redux/FetchDataSaga' import {loadFollows} from 'app/redux/FollowSaga' import { signData } from 'golos-lib-js/lib/auth' import {PrivateKey, Signature, hash} from 'golos-lib-js/lib/auth/ecc' -import {api} from 'golos-lib-js' +import {api, config} from 'golos-lib-js' import g from 'app/redux/GlobalReducer' import React from 'react'; import PushNotificationSaga from 'app/redux/services/PushNotificationSaga'; @@ -170,7 +170,14 @@ function* usernamePasswordLogin2({payload: {username, password, saveLogin, const isRole = (role, fn) => (!userProvidedRole || role === userProvidedRole ? fn() : undefined) - let account = yield call(getAccount, username) + let account + try { + account = yield call(getAccount, username) + } catch (err) { + console.error(err) + yield put(user.actions.loginError({ error: 'Node failure', node: config.get('websocket') })) + return + } if (!account) { yield put(user.actions.loginError({ error: 'Username does not exist' })) return diff --git a/app/redux/UserSaga_UploadImage.js b/app/redux/UserSaga_UploadImage.js index b0f73c763..654ba7ee0 100644 --- a/app/redux/UserSaga_UploadImage.js +++ b/app/redux/UserSaga_UploadImage.js @@ -2,8 +2,9 @@ import tt from 'counterpart'; import { select, takeEvery } from 'redux-saga/effects'; import { signData } from 'golos-lib-js/lib/auth' import { Signature, hash } from 'golos-lib-js/lib/auth/ecc/index'; +import { Asset, fetchEx } from 'golos-lib-js/lib/utils' -const MAX_UPLOAD_IMAGE_SIZE = 1024 * 1024; +const MAX_UPLOAD_IMAGE_SIZE = 1; export default function* uploadImageWatch() { yield takeEvery('user/UPLOAD_IMAGE', uploadImage); @@ -31,7 +32,7 @@ const ERRORS_MATCH = [ ]; function* uploadImage(action) { - const { file, dataUrl, filename = 'image.txt', progress, useGolosImages = false } = action.payload; + const { file, dataUrl, filename = 'image.txt', progress, imageSizeLimit = 0 } = action.payload; function onError(txt) { progress({ @@ -54,7 +55,7 @@ function* uploadImage(action) { let data, dataBase64; if (file) { - const reader = new FileReader(); + const reader = new (window.FileReader0 || FileReader)(); data = yield new Promise(resolve => { reader.addEventListener('load', () => { @@ -69,29 +70,61 @@ function* uploadImage(action) { } let postUrl = $STM_Config.images.upload_image + let golosImages = false - if (file && file.size > MAX_UPLOAD_IMAGE_SIZE) { - if (useGolosImages && $STM_Config.images.use_img_proxy !== false) { - const user = yield select(state => state.user); - const username = user.getIn(['current', 'username']); - const postingKey = user.getIn([ - 'current', - 'private_keys', - 'posting_private', - ]); - if (!username || !postingKey) { - onError(tt('user_saga_js.image_upload.error.login_first')); - return; - } - const signatures = signData(data, { - posting: postingKey, - }) - postUrl = new URL('/@' + username + '/' + signatures.posting, $STM_Config.images.img_proxy_prefix).toString(); - golosImages = true - } else { - onError(tt('user_saga_js.image_upload.error.image_size_is_too_large')); + const user = yield select(state => state.user) + const switchToGolosImages = async () => { + const username = user.getIn(['current', 'username']); + const postingKey = user.getIn([ + 'current', + 'private_keys', + 'posting_private', + ]); + if (!username || !postingKey) { + onError(tt('user_saga_js.image_upload.error.login_first')); return; } + const signatures = signData(data, { + posting: postingKey, + }) + postUrl = new URL('/@' + username + '/' + signatures.posting, $STM_Config.images.img_proxy_prefix).toString(); + golosImages = true + } + + if (file) { + if (imageSizeLimit && file.size > imageSizeLimit) { + onError(tt('user_saga_js.image_upload.error.image_size_is_too_large')); + return + } + + if ($STM_Config.images.use_img_proxy !== false) { + let recommended = false + try { + let su = new URL('/start_upload/' + file.size, $STM_Config.images.img_proxy_prefix).toString() + su = yield fetchEx(su, { + timeout: 1500 + }) + su = yield su.json() + if (su.recommended === 'undefined') { + console.warning('image_proxy start_upload:', 'No recommended field:', su) + throw new Error('Wrong response') + } + recommended = !!su.recommended + } catch (err) { + console.error('image_proxy start_upload:', err) + } + if (recommended) { + yield switchToGolosImages() + } + } + } + const onImgurFail = async (imgurErr) => { + console.log('onImgurFail - switch to Golos Images..') + await switchToGolosImages() + console.log('onImgurFail - ok, sending..') + xhr.open('POST', postUrl); + formData.append('fallback', imgurErr) + xhr.send(formData) } /** @@ -115,12 +148,33 @@ function* uploadImage(action) { const xhr = new XMLHttpRequest(); + let tm + let connected + const clearTm = () => { + if (tm) clearTimeout(tm) + } + const resetTm = () => { + clearTm() + tm = setTimeout(() => { + onError(tt('user_saga_js.image_upload.error.upload_failed')) + xhr.abort() + }, connected ? 10000 : 5000) + } + resetTm() + xhr.open('POST', postUrl); if (!golosImages) { xhr.setRequestHeader('Authorization', 'Client-ID ' + $STM_Config.images.client_id) } - xhr.onload = function() { + xhr.onloadstart = function () { + connected = true + resetTm() + } + + xhr.onload = async function() { + clearTm() + let data; try { @@ -147,23 +201,40 @@ function* uploadImage(action) { console.error('Cannot upload image:', xhr.responseText); - let repeat = false; if (!golosImages) { if (xhr.responseText.includes('Invalid client')) { ++imgurFailCounter; if (imgurFailCounter < 5) { - repeat = true; setTimeout(() => { xhr.open('POST', postUrl); xhr.setRequestHeader('Authorization', 'Client-ID ' + $STM_Config.images.client_id) xhr.send(formData); }, 1000); + return } } + if (!xhr.responseText.includes('file type invalid') + && !xhr.responseText.includes('We don\'t support that file type!')) { + await onImgurFail(xhr.responseText) + return + } } - if (!repeat) { - onError(xhr.responseText); + + let err = xhr.responseText + if (golosImages) { + if (data.error === 'too_low_account_golos_power') { + const need = Asset(data.required) + const add = need.minus(Asset(data.power)) + err = tt('user_saga_js.image_upload.error.low_golos_power_NEED_ADD', + { + NEED: need.floatString, + ADD: add.floatString + }) + } else if (data.error === 'too_low_account_reputation') { + err = tt('user_saga_js.image_upload.error.low_reputation') + data.required + } } + onError(err) } else { let result = {} if (!golosImages) { @@ -180,11 +251,13 @@ function* uploadImage(action) { }; xhr.onerror = function(error) { + clearTm() onError(tt('user_saga_js.image_upload.error.server_unavailable')); console.error(error); }; xhr.upload.onprogress = function(event) { + resetTm() if (event.lengthComputable) { const percent = Math.round((event.loaded / event.total) * 100); diff --git a/app/redux/services/PushNotificationSaga.js b/app/redux/services/PushNotificationSaga.js index aa4db98da..ae164fc73 100644 --- a/app/redux/services/PushNotificationSaga.js +++ b/app/redux/services/PushNotificationSaga.js @@ -27,11 +27,26 @@ function getScopePresets(username) { if (presets.donate) { presets.donate_msgs = true } - return Object.keys(presets).filter(k => presets[k]); + + let bgPresets = [] + if (process.env.MOBILE_APP) { + const forApp = ['comment_reply', 'mention'] + for (const p of forApp) { + if (presets[p]) bgPresets.push(p) + } + if (presets.in_background === undefined) { + presets.in_background = true + } + } + const inBackground = presets.in_background + delete presets.in_background + return { presets: Object.keys(presets).filter(k => presets[k]), + bgPresets, + inBackground } } export function* onUserLogin(action) { - let presets = getScopePresets(action.username).join(','); + let presets = getScopePresets(action.username).presets.join(','); if (!presets) { console.log('GNS: all scopes disabled, so will not subscribe'); @@ -72,7 +87,7 @@ export function* onUserLogin(action) { removeTaskIds = yield notificationTake(action.username, removeTaskIds, (type, op, timestamp, id, scope) => { if (op._offchain) return; - if (!getScopePresets(action.username).includes(scope)) { + if (!getScopePresets(action.username).presets.includes(scope)) { return; } if (scope === 'message') { @@ -130,4 +145,5 @@ export function* pushNotificationWatches() { export default { onUserLogin, pushNotificationWatches, + getScopePresets, } diff --git a/app/renderApp.js b/app/renderApp.js index 43adae76d..e05e5e9ae 100644 --- a/app/renderApp.js +++ b/app/renderApp.js @@ -34,6 +34,9 @@ export default async function renderApp(initialState) { clientRender(initialState) } catch (error) { console.error(error) + if (process.env.MOBILE_APP) { + alert('renderApp ' + error.toString() + '\n' + JSON.stringify(error.stack)) + } serverApiRecordEvent('client_error', error) } } \ No newline at end of file diff --git a/app/utils/ContentAccess.js b/app/utils/ContentAccess.js index 9a753f56d..7d8ecf238 100644 --- a/app/utils/ContentAccess.js +++ b/app/utils/ContentAccess.js @@ -7,7 +7,7 @@ function hasLS() { } export const getFilterApps = () => { - return ['freedom.blog'] + return $STM_Config.filter_apps } export function loadNsfwSettings(username) { diff --git a/app/utils/ScreenSize.jsx b/app/utils/ScreenSize.jsx new file mode 100644 index 000000000..926743958 --- /dev/null +++ b/app/utils/ScreenSize.jsx @@ -0,0 +1,75 @@ +import React from 'react' + +const isScreenS = () => { + const res = window.matchMedia('screen and (max-width: 39.9375em)').matches + return res +} + +const shortQuestion = '63.9375em' +const hideOrders = '710px' +const hideOrdersMe = '768px' + +const hideMainMe = '800px' +const hideRewardsMe = '440px' + +const hideMainFor = '560px' +const hideRewardsFor = '440px' + +const isSmaller = (val) => { + const res = window.matchMedia('screen and (max-width: ' + val + ')').matches + return res +} + +export const withScreenSize = (WrappedComponent) => { + class ScreenSize extends React.Component { + state = {} + + componentDidMount() { + if (!process.env.BROWSER) return + this.updateSize() + window.addEventListener('resize', this.onResize) + } + + componentWillUnmount() { + if (!process.env.BROWSER) return + window.removeEventListener('resize', this.onResize) + } + + onResize = () => { + this.updateSize() + } + + updateSize = () => { + this.setState({ + _isSmall: isScreenS(), + _shortQuestion: isSmaller(shortQuestion), + _hideOrders: isSmaller(hideOrders), + _hideOrdersMe: isSmaller(hideOrdersMe), + _hideMainMe: isSmaller(hideMainMe), + _hideRewardsMe: isSmaller(hideRewardsMe), + _hideMainFor: isSmaller(hideMainFor), + _hideRewardsFor: isSmaller(hideRewardsFor), + }) + } + + render() { + const { _shortQuestion, _hideOrders, _hideOrdersMe, + _hideMainMe, _hideRewardsMe, _hideMainFor, _hideRewardsFor, } = this.state + return ( + + ) + } + } + + return ScreenSize +} diff --git a/app/utils/app/RoutingUtils.js b/app/utils/app/RoutingUtils.js new file mode 100644 index 000000000..dfd93da2f --- /dev/null +++ b/app/utils/app/RoutingUtils.js @@ -0,0 +1,65 @@ +import { browserHistory, } from 'react-router' + +export function reloadLocation(href) { + if (href && href[0] === '#') { + throw new Error('reloadLocation cannot reload with href starts with #') + } + const { MOBILE_APP } = process.env + if (MOBILE_APP) { + let { pathname, hash, host } = window.location + if (href) { + if (href.startsWith('http:') || href.startsWith('https:')) { + const url = new URL(href) + if (url.host !== host) { + window.open(href, '_blank') + // And just opening in same tab - not working, somewhy opens with app's hostname... + return + } + href = url.pathname + url.search + url.hash + } + if (href[0] !== '/') { + href = '/' + href + } + } else { + href = pathname || '/' + } + window.location.href = '/#' + href + if (!pathname || pathname === '/') { + window.location.reload() + } + return + } + window.location.href = href +} + +export function hrefClick(e) { + if (process.env.MOBILE_APP) { + let node, href, target + do { + node = node ? node.parentNode : e.target + if (!node) break + href = node.href + target = node.target + } while (!href) + if (!href) return + e.preventDefault() + reloadLocation(href) + } +} + +//... and this processes such reloads: + +export function fixRouteIfApp() { + const { MOBILE_APP } = process.env + if (!MOBILE_APP) { + return true + } + let hash = window.location.hash + if (hash && hash[1] === '/') { + hash = hash.slice(1) + if (!hash) hash = '/' + browserHistory.push(hash) + return false + } + return true +} diff --git a/app/utils/app/ShortcutUtils.js b/app/utils/app/ShortcutUtils.js new file mode 100644 index 000000000..35c3b524a --- /dev/null +++ b/app/utils/app/ShortcutUtils.js @@ -0,0 +1,84 @@ + +async function getShorcuts() { + let i = 0 + for (let i = 0; i < 5; ++i) { + if (!window.plugins || !window.plugins.Shortcuts) { + await new Promise(resolve => setTimeout(resolve, 50)) + continue + } + const { Shortcuts } = window.plugins + return Shortcuts + } + return null +} + +async function dynShortcutsSupported() { + const Shortcuts = await getShorcuts() + return await new Promise(async (resolve, reject) => { + Shortcuts.supportsDynamic((supported) => { + resolve(supported) + }, (err) => { + reject(err) + }) + }) +} + +async function setDynShortcut(shortcut) { + const Shortcuts = await getShorcuts() + await new Promise(async (resolve, reject) => { + Shortcuts.setDynamic([shortcut], () => { + resolve() + }, (err) => { + reject(err) + }) + }) +} + +export async function addShortcut({ id, shortLabel, longLabel, hash }) { + try { + let shortcutSupport = await dynShortcutsSupported() + if (!shortcutSupport) { + console.error('Cannot add shortcut - not supported') + return + } + let shortcut = { + id, + shortLabel, + longLabel, + iconFromResource: 'blg_setting', + intent: { + action: 'android.intent.action.RUN', + flags: 67108864, // FLAG_ACTIVITY_CLEAR_TOP + extras: { + id: Math.random().toString(), + hash + } + } + } + await setDynShortcut(shortcut) + console.log('Shortcut successfully created') + } catch (err) { + console.error('Adding shortcut failed with', err) + } +} + +export async function getShortcutIntent() { + const Shortcuts = await getShorcuts() + return await new Promise((resolve, reject) => { + try { + Shortcuts.getIntent(intent => { + resolve(intent) + }) + } catch (err) { + reject(err) + } + }) +} + +export async function onShortcutIntent(handler) { + const Shortcuts = await getShorcuts() + Shortcuts.onNewIntent() + Shortcuts.onNewIntent((intent) => { + handler(intent) + }) +} diff --git a/app/utils/app/UpdateUtils.js b/app/utils/app/UpdateUtils.js new file mode 100644 index 000000000..6af0dbf70 --- /dev/null +++ b/app/utils/app/UpdateUtils.js @@ -0,0 +1,110 @@ +import tt from 'counterpart' +import { fetchEx } from 'golos-lib-js/lib/utils' + +function updaterHost() { + return $STM_Config.app_updater.host +} + +async function httpGet(url, timeout = fetchEx.COMMON_TIMEOUT, responseType = 'text') { + if (process.env.MOBILE_APP) { + return await new Promise((resolve, reject) => { + try { + cordova.plugin.http.sendRequest(url.toString(), { + responseType, + timeout: Math.ceil(timeout / 1000) + }, (resp) => { + resolve(resp.data) + }, (resp) => { + reject(resp.error) + }) + } catch (err) { + reject(err) + } + }) + } else { + let res = await fetchEx(url, { + timeout + }) + if (responseType === 'arraybuffer') { + res = await res.arrayBuffer() + } else { + res = await res.text() + } + return res + } +} + +export async function checkUpdates(timeout = 2000) { + let url = '' + try { + let path + const isDesktop = !process.env.MOBILE_APP + if (isDesktop) { + path = 'desktop/' + ($STM_Config.platform === 'linux' ? 'linux' : 'windows') + } else { + path = 'messenger/android' + } + url = new URL( + '/api/' + path, updaterHost() + ) + url.searchParams.append('latest', '1') + url.searchParams.append('after', $STM_Config.app_version) + let res = await httpGet(url, timeout) + res = JSON.parse(res) + if (res.status === 'ok' && res.data) { + const versions = Object.entries(res.data) + if (versions[0]) { + const [ v, obj ] = versions[0] + if (obj.exe) { + let link = '/__app_update?v=' + v + '&exe=' + obj.exe + '&txt=' + obj.txt + if (!isDesktop) { + link = new URL('/api/html/' + path + '/' + v, updaterHost()) + link = link.toString() + } + return { + show: true, + id: v, + link, + title: tt('app_update.notify_VERSION', { VERSION: v }), + new_tab: true, + } + } else { + console.error(versions[0]) + } + } + } else { + if (res.error) { + if (process.env.MOBILE_APP) { + throw new Error(res.error) + } + } + console.error(res) + } + } catch (err) { + if (process.env.MOBILE_APP) { + throw new Error((err || 'checkUpdates') + ' (' + url + ')', + { cause : err }) + } else { + console.error('checkUpdates', err) + } + } + return {} +} + +export async function getChangelog(txtLink) { + try { + const decoder = new TextDecoder('windows-1251') + let res + if (process.env.MOBILE_APP) { + res = await httpGet(txtLink, 1000, 'arraybuffer') + res = decoder.decode(res) + } else { + res = await fetch(txtLink) + res = decoder.decode(await res.arrayBuffer()) + } + return res + } catch (err) { + console.error('getChangelog', err) + throw err + } +} diff --git a/app/utils/sponsors.js b/app/utils/sponsors.js index 7cfd3f1fe..532f0cd86 100644 --- a/app/utils/sponsors.js +++ b/app/utils/sponsors.js @@ -119,7 +119,7 @@ export async function tryDecryptContents(contents) { }) const convertRes = (content, res) => { - const { body, err, sub, } = res + const { body, err, sub, decrypt_fee } = res if (body) { content.body = body content.encrypted = EncryptedStates.decrypted @@ -135,8 +135,8 @@ export async function tryDecryptContents(contents) { content.encrypted = EncryptedStates.unknown } content.encrypted_sub = sub + content.encrypted_decrypt_fee = decrypt_fee } - if (entries.length) { try { const { head_block_number, witness } = await golos.api.getDynamicGlobalPropertiesAsync() diff --git a/build_app_entry.js b/build_app_entry.js index c2af0a1a7..87bf821b1 100644 --- a/build_app_entry.js +++ b/build_app_entry.js @@ -5,11 +5,11 @@ const fse = require('fs-extra') const config = require('config') import ServerHTML from './server/server-html'; -const app_version = require('./package.json').version +const blogs_version = require('./package.json').version let destDir, cfgFile const argv = process.argv -if (argv.length !== 4) { +if (argv.length < 4) { console.log('Usage is: babel-node build_app_entry.js /path/to/build/dest /path/to/config') process.exit(-1) } @@ -29,11 +29,18 @@ if (destDir !== 'null') { fse.copySync('app/assets/images', destDir + '/images', { overwrite: true }) // for some direct links } +if (cfgFile === '_mobile') { + process.exit(0) +} + let cfg = {} const copyKey = (key) => { cfg[key] = config.get('desktop.' + key) } -cfg.app_version = app_version +cfg.blogs_version = blogs_version +if (argv[4]) cfg.app_version = argv[4] +if (argv[5]) cfg.wallet_version = argv[5] +if (argv[6]) cfg.msgs_version = argv[6] copyKey('site_domain') cfg.url_domains = [...config.get('desktop.another_domains')] if (!cfg.url_domains.includes(cfg.site_domain)) { diff --git a/config.xml b/config.xml new file mode 100644 index 000000000..151ded74a --- /dev/null +++ b/config.xml @@ -0,0 +1,37 @@ + + + GOLOS Блоги + Мобильное приложение Блогов на блокчейне Golos. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/blacklist.json b/config/blacklist.json index f46484c66..f068aac46 100644 --- a/config/blacklist.json +++ b/config/blacklist.json @@ -13,5 +13,8 @@ "/ru--kino/@nichosee/devushka-podayushaya-nadezhdy-promising-young-woman", "/ru--kino/@huckster/kosmicheskii-styob-pank-serial-2021-kovboi-bibop", "/ru--kino/@id-svetpro/nebo-2021" + ], + "filter_apps": [ + "freedom.blog" ] } diff --git a/config/mobile.json b/config/mobile.json new file mode 100644 index 000000000..d173bd35c --- /dev/null +++ b/config/mobile.json @@ -0,0 +1,62 @@ +{ + "mobile": { + "ws_connection_app": [ + { "address": "wss://apibeta.golos.today/ws" }, + { "address": "wss://api.golos.id/ws" }, + { "address": "wss://api.aleksw.space/ws" }, + { "address": "wss://api-golos.blckchnd.com/ws" } + ], + "ws_connection_exchange": "wss://apibeta.golos.today/ws", + "site_domain": "beta.golos.today", + "logo": { + "icon": "https://i.imgur.com/Q7GCdPf.png", + "title": "https://i.imgur.com/36zv8We.png" + }, + "images": { + "img_proxy_prefix": "https://devimages.golos.today", + "img_proxy_backup_prefix": "https://steemitimages.com", + "upload_image": "https://api.imgur.com/3/image", + "client_id": "6c09ebf8c548126" + }, + "wallet_service": { + "host": "https://devwallet.golos.today" + }, + "messenger_service": { + "host": "https://devchat.golos.app" + }, + "auth_service": { + "host": "https://dev.golos.app", + "custom_client": "blogs" + }, + "notify_service": { + "host": "https://devnotify.golos.app", + "host_ws": "wss://devnotify.golos.app/ws" + }, + "elastic_search": { + "url": "https://search.golos.today", + "login": "golosclient", + "password": "golosclient" + }, + "apidex_service": { + "host": "https://devapi-dex.golos.app", + "host_local": "https://devapi-dex.golos.app" + }, + "app_updater": { + "host": "https://files.golos.app" + }, + "forums": { + "white_list": ["fm-golostalk", "fm-prizmtalk", "fm-graphenetalks"], + "fm-golostalk": {"domain": "golostalk.com"}, + "fm-prizmtalk": {"domain": "prizmtalk.com"}, + "fm-graphenetalks": {"domain": "forum.gph.ai"} + }, + "hidden_assets": { + "RUDEX": true, + "PRIZM": true, + "DOGECOIN": true, + "YMZEC": true, + "YMWMZ": true, + "YMBTC": true + } + } +} diff --git a/native_core/package.json b/native_core/package.json new file mode 100644 index 000000000..3f854d9de --- /dev/null +++ b/native_core/package.json @@ -0,0 +1,4 @@ +{ + "name": "gls-blogs-native-core", + "version": "1.0.0" +} diff --git a/native_core/plugin.xml b/native_core/plugin.xml new file mode 100644 index 000000000..b6e9a8cb3 --- /dev/null +++ b/native_core/plugin.xml @@ -0,0 +1,46 @@ + + + Golos Blogs Native Core + Provides notification service when activity is paused, as well as on boot received + Apache 2.0 + cordova + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/native_core/res/ic_empty.xml b/native_core/res/ic_empty.xml new file mode 100644 index 000000000..16f82337d --- /dev/null +++ b/native_core/res/ic_empty.xml @@ -0,0 +1,10 @@ + + + + diff --git a/native_core/res/notify.png b/native_core/res/notify.png new file mode 100644 index 000000000..4ad196057 Binary files /dev/null and b/native_core/res/notify.png differ diff --git a/native_core/src/android/ApiClient.kt b/native_core/src/android/ApiClient.kt new file mode 100644 index 000000000..70a7cd928 --- /dev/null +++ b/native_core/src/android/ApiClient.kt @@ -0,0 +1,50 @@ +package gls.blogs.core + +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.json.JSONObject +import java.util.concurrent.TimeUnit + +open class ApiClient(val host: String) { + private var client: OkHttpClient = OkHttpClient() + + fun urlBuilder(): HttpUrl.Builder { + return host.toHttpUrl().newBuilder() + } + + open fun reqBuilder(url: HttpUrl): Request.Builder { + return Request.Builder() + .url(url) + } + + private fun call(req: Request, err: String, timeoutMsec : Long? = null) : Response { + try { + var clientBuilder = client.newBuilder() + if (timeoutMsec != null) { + clientBuilder = clientBuilder.readTimeout(timeoutMsec, TimeUnit.MILLISECONDS) + } + return clientBuilder.build().newCall(req).execute() + } catch (e: Exception) { + e.printStackTrace() + throw Exception(err) + } + } + + private fun getJSON(str: String, err: String) : JSONObject { + try { + return JSONObject(str) + } catch (e: Exception) { + e.printStackTrace() + throw Exception(err) + } + } + + fun callForJSON(req: Request, err: String, timeoutMsec : Long? = null) : JSONObject { + val res = call(req, err, timeoutMsec) + val str = res.body?.string() + return getJSON(str!!, err) + } +} \ No newline at end of file diff --git a/native_core/src/android/AppPrefs.kt b/native_core/src/android/AppPrefs.kt new file mode 100644 index 000000000..ab2a37361 --- /dev/null +++ b/native_core/src/android/AppPrefs.kt @@ -0,0 +1,9 @@ +package gls.blogs.core + +data class AppPrefs( + var account: String = "", + var session: String = "", + var scopes: String = "", + var lastTake: Long = 0, + var notifyHost: String = "" + ) \ No newline at end of file diff --git a/native_core/src/android/BootReceiver.kt b/native_core/src/android/BootReceiver.kt new file mode 100644 index 000000000..8dce2e8d8 --- /dev/null +++ b/native_core/src/android/BootReceiver.kt @@ -0,0 +1,31 @@ +package gls.blogs.core + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log + +const val TAG = "GLS/BootReceiver" + +class BootReceiver: BroadcastReceiver() { + override fun onReceive(ctx: Context?, intent: Intent?) { + if (intent == null) { + Log.e(TAG, "Boot received, but no Intent") + return + } + if (intent.action != Intent.ACTION_BOOT_COMPLETED) { + Log.e(TAG, "Boot received, but Intent has wrong action: ${intent.action}") + return + } + if (ctx != null) { + if (ServiceHelper.loadPrefs(ctx).account != "") { + Log.i(TAG, "Boot received, starting service") + ServiceHelper.startNotifyService(ctx.applicationContext) + } else { + Log.w(TAG, "Boot received, but account in prefs is null, so we should not start service") + } + } else { + Log.e(TAG, "Boot received, but no Context, so cannot start service") + } + } +} \ No newline at end of file diff --git a/native_core/src/android/CorePlugin.kt b/native_core/src/android/CorePlugin.kt new file mode 100644 index 000000000..6991c187b --- /dev/null +++ b/native_core/src/android/CorePlugin.kt @@ -0,0 +1,34 @@ +package gls.blogs.core + +import android.content.Context +import org.apache.cordova.CordovaPlugin +import org.apache.cordova.CallbackContext + +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import android.widget.Toast + +class CorePlugin : CordovaPlugin() { + override fun execute(action: String, args: JSONArray, callbackContext: CallbackContext) : Boolean { + val ctx = this.cordova.getContext() + if (action.equals("startService")) { + var prefs = AppPrefs() + prefs.account = args.getString(0) + prefs.session = args.getString(1) + prefs.scopes = args.getString(2) + prefs.lastTake = args.getLong(3) + prefs.notifyHost = args.getString(4) + // And not passing subscriber id because service should subscribe again + ServiceHelper.savePrefs(ctx, prefs) + ServiceHelper.startNotifyService(ctx) + callbackContext.success() + } else if (action.equals("stopService")) { + ServiceHelper.stopNotifyService(ctx) + callbackContext.success() + } else if (action.equals("logout")) { + ServiceHelper.clearPrefs(ctx) + } + return false + } +} \ No newline at end of file diff --git a/native_core/src/android/NotificationHelper.kt b/native_core/src/android/NotificationHelper.kt new file mode 100644 index 000000000..0d8b46797 --- /dev/null +++ b/native_core/src/android/NotificationHelper.kt @@ -0,0 +1,97 @@ +package gls.blogs.core + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.media.RingtoneManager +import android.net.Uri +import android.os.Build +import android.widget.Toast +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import gls.blogs.MainActivity +import gls.blogs.R + +private const val MESSAGES_CHANNEL = "MESSAGES" +private const val MESSAGE_NOTIFICATION_ID = 2 + +private const val FOREGROUND_CHANNEL = "FOREGROUND" +const val FOREGROUND_NOTIFICATION_ID = 1 + +class NotificationHelper(val ctx: Context) { + private val soundUri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + private val notificationManager: NotificationManager = + ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private var notifId = MESSAGE_NOTIFICATION_ID + + private fun createChannel(id: String, name: String, description: String, importance: Int, silent: Boolean = false) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel(id, name, importance).apply { + description + } + if (!silent) { + channel.enableVibration(true) + channel.setSound(soundUri, null) + } + notificationManager.createNotificationChannel(channel) + } + } + + private fun createMessagesChannel() { + createChannel(MESSAGES_CHANNEL, "Messages", "New messages notifications", + NotificationManager.IMPORTANCE_HIGH) + } + + private fun createForegroundChannel() { + createChannel( + FOREGROUND_CHANNEL, "Others", "Others channel", + NotificationManager.IMPORTANCE_LOW, true) + } + + private fun pendingIntent() : PendingIntent { + val intent = Intent(ctx, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + return PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE) + } + + private fun makeMessage(title: String, description: String, icon: Int): Notification { + createMessagesChannel() + + return NotificationCompat.Builder(ctx, MESSAGES_CHANNEL) + .setSmallIcon(icon) + .setContentTitle(title) + .setContentText(description) + .setContentIntent(pendingIntent()) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setSound(soundUri) + .build() + } + + fun notifyMessage(title: String, description: String) { + val notification = makeMessage(title, description, R.drawable.notify) + + with(NotificationManagerCompat.from(ctx)) { + if (notifId >= MESSAGE_NOTIFICATION_ID+2) { + notifId = MESSAGE_NOTIFICATION_ID + } + notificationManager.notify(notifId++, notification) + } + } + + fun makeForeground(title: String, description: String, icon: Int): Notification { + createForegroundChannel() + + return NotificationCompat.Builder(ctx, FOREGROUND_CHANNEL) + .setSmallIcon(icon) + .setContentTitle(title) + .setContentText(description) + .setContentIntent(pendingIntent()) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build() + } +} \ No newline at end of file diff --git a/native_core/src/android/NotifyApiClient.kt b/native_core/src/android/NotifyApiClient.kt new file mode 100644 index 000000000..cb45b4925 --- /dev/null +++ b/native_core/src/android/NotifyApiClient.kt @@ -0,0 +1,88 @@ +package gls.blogs.core + +import android.util.Log +import okhttp3.HttpUrl +import okhttp3.Request +import org.json.JSONObject + +class NotifyApiClient(notifyHost: String) : ApiClient(notifyHost) { + var session = "" + + override fun reqBuilder(url: HttpUrl): Request.Builder { + val builder = super.reqBuilder(url) + return builder.header("X-Session", session) + } + + public fun subscribe(acc: String, scopes: String) : Int { + val url = urlBuilder().addPathSegments("subscribe/@$acc/$scopes").build() + val request = reqBuilder(url).build() + + val json = callForJSON(request, "Cannot subscribe") + try { + return json.getInt("subscriber_id") + } catch (e: Exception) { + throw Exception("Cannot subscribe, error: " + json.optString("error")) + } + } + + data class TakeResult( + var removeTaskIds: ArrayList = ArrayList(), + var lastTake: Long = 0 + ) {} + + public fun take(acc: String, subId: String, callback: (String, JSONObject) -> Unit, removeTaskIds: String?): TakeResult { + var builder = urlBuilder().addPathSegments("take/@$acc/$subId") + if (removeTaskIds != null) { + builder = builder.addPathSegment(removeTaskIds) + } + + val request = reqBuilder(builder.build()).build() + + val json = callForJSON(request, "Cannot take", 61000) + try { + val lastTake = json.getLong("__") + val tasks = json.getJSONArray("tasks") + var removeTaskIds = ArrayList() + (0 until tasks.length()).forEach { + val task = tasks.getJSONObject(it) + + val data = task.getJSONArray("data") + val type = data.getString(0) + val op = data.getJSONObject(1) + + callback(type, op) + + val taskId = task.getString("id") + removeTaskIds.add(taskId) + } + return TakeResult(removeTaskIds, lastTake) + } catch (e: Exception) { + e.printStackTrace() + throw Exception(json.getString("error")) + } + } + + public fun getInbox(acc: String, lastTake: Long, callback: (JSONObject) -> Unit) { + var builder = urlBuilder().addPathSegments("msgs/get_inbox/@$acc") + .addQueryParameter("unread_only", "true") + val request = reqBuilder(builder.build()).build() + val json = callForJSON(request, "Cannot getInbox") + try { + val results = json.getJSONArray("result") + for (index in 0 until results.length()) { + val result = results.getJSONObject(index) + + val time = result.getLong("__time") + + if (time >= lastTake) { + callback(result) + } else { + break + } + } + } catch (e: Exception) { + e.printStackTrace() + throw Exception(json.getString("error")) + } + } +} \ No newline at end of file diff --git a/native_core/src/android/NotifyService.kt b/native_core/src/android/NotifyService.kt new file mode 100644 index 000000000..5155cb33c --- /dev/null +++ b/native_core/src/android/NotifyService.kt @@ -0,0 +1,132 @@ +package gls.blogs.core + +import android.app.Service +import android.content.Intent +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.widget.Toast +import android.util.Log +import org.json.JSONObject +import kotlin.concurrent.thread +import gls.blogs.R + +class NotifyService() : Service() { + companion object { + private const val TAG = "GLS/NotifyService" + + const val ACTION_STOP = "ACTION_STOP" + } + + private lateinit var nac: NotifyApiClient + private lateinit var nh: NotificationHelper + private lateinit var prefs: AppPrefs + private var workThread: Thread? = null + + private var subId = "" + + private fun showNotification(descr: String) { + Handler(Looper.getMainLooper()).post { + nh.notifyMessage("GOLOS Блоги", descr) + } + } + + private fun doLoop(removeTaskIds: ArrayList) { + if (Thread.interrupted()) return + + if (subId.isEmpty()) { + try { + subId = nac?.subscribe(prefs.account, prefs.scopes).toString() + } catch (e: Exception) { + e.printStackTrace() + Thread.sleep(5000) + doLoop(removeTaskIds) + return + } + Log.i(TAG, "NotifyService subscribed $subId") + + if (Thread.interrupted()) return + } + + if (Thread.interrupted()) return + + var entries = ArrayList() + val rti = removeTaskIds.joinToString(",") + var newRTI = ArrayList() + try { + val takeRes = nac?.take(prefs.account, subId, { typ: String, op: JSONObject -> + var entry = typ + ":" + op.toString() + val author = op.optString("author", "") + val parent_author = op.optString("parent_author", "") + if (typ == "comment_reply") { + val _depth = op.optInt("_depth", 0) + if (_depth > 1) { + entry = "@" + author + " ответил на ваш комментарий." + } else { + entry = "@" + author + " прокомментировал ваш пост." + } + } else if (typ == "comment_mention") { + if (parent_author != "") { + entry = "@" + author + " упомянул вас в комментарии." + } else { + entry = "@" + author + " упомянул вас в своем посте." + } + } + entries.add(entry) + }, rti) + newRTI = takeRes.removeTaskIds + + if (Thread.interrupted()) return + + prefs.lastTake = takeRes.lastTake + ServiceHelper.savePrefs(applicationContext, prefs) + + for (i in 0..entries.size) { + showNotification(entries[i]) + } + } catch (e: Exception) { + e.printStackTrace() + if (e.message != null && e.message!!.contains("No such queue")) { + Log.e(TAG, "No such queue - resubscribing") + subId = "" + } + } + Thread.sleep(2500) + doLoop(newRTI) + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent != null && intent.action == ACTION_STOP) { + if (workThread != null) { + workThread!!.interrupt() + stopForeground(true) + stopSelfResult(startId) + } + return START_STICKY + } + + nh = NotificationHelper(this) + val n = nh.makeForeground(" ", "GOLOS Блоги работают.", R.drawable.ic_empty) + startForeground(FOREGROUND_NOTIFICATION_ID, n) + + Log.i(TAG, "Started") + + prefs = ServiceHelper.loadPrefs(applicationContext) + nac = NotifyApiClient(prefs.notifyHost) + nac.session = prefs.session + + workThread = thread { + try { + doLoop(ArrayList()) + } catch (e: InterruptedException) { + Log.i(TAG, "Service stopped - InterruptedException", e) + } + } + + return START_STICKY + } +} diff --git a/native_core/src/android/ServiceHelper.kt b/native_core/src/android/ServiceHelper.kt new file mode 100644 index 000000000..6c4c59e1a --- /dev/null +++ b/native_core/src/android/ServiceHelper.kt @@ -0,0 +1,70 @@ +package gls.blogs.core + +import android.app.Application +import android.content.Context +import android.content.Context.MODE_PRIVATE +import android.content.Intent +import android.content.SharedPreferences + +const val PREF_ACCOUNT = "pref_account" +const val PREF_SESSION = "pref_session" +const val PREF_SCOPES = "pref_scopes" +const val PREF_LAST_TAKE = "pref_last_take" +const val PREF_NOTIFYHOST = "pref_notify_host" + +class ServiceHelper { + companion object { + private fun getSharedPrefs(ctx: Context): SharedPreferences { + return ctx.getSharedPreferences("prefs", MODE_PRIVATE) + } + + fun savePrefs(ctx: Context, prefs: AppPrefs) { + val sharedPrefs = getSharedPrefs(ctx) + with (sharedPrefs.edit()) { + putString(PREF_ACCOUNT, prefs.account) + putString(PREF_SESSION, prefs.session) + putString(PREF_SCOPES, prefs.scopes) + putLong(PREF_LAST_TAKE, prefs.lastTake) + putString(PREF_NOTIFYHOST, prefs.notifyHost) + apply() + } + } + + fun loadPrefs(ctx: Context): AppPrefs { + val sharedPrefs = getSharedPrefs(ctx) + return AppPrefs( + sharedPrefs.getString(PREF_ACCOUNT, "")!!, + sharedPrefs.getString(PREF_SESSION, "")!!, + sharedPrefs.getString(PREF_SCOPES, "")!!, + sharedPrefs.getLong(PREF_LAST_TAKE, 0), + sharedPrefs.getString(PREF_NOTIFYHOST, "")!! + ) + } + + fun clearPrefs(ctx: Context) { + val sharedPrefs = getSharedPrefs(ctx) + with (sharedPrefs.edit()) { + remove(PREF_ACCOUNT) + remove(PREF_SESSION) + remove(PREF_SCOPES) + remove(PREF_LAST_TAKE) + remove(PREF_NOTIFYHOST) + apply() + } + } + + fun startNotifyService(ctx: Context?) { + if (ctx != null) { + ctx.startForegroundService(Intent(ctx, NotifyService::class.java)) + } + } + + fun stopNotifyService(ctx: Context?) { + if (ctx != null) { + val intent = Intent(ctx, NotifyService::class.java) + intent.action = NotifyService.ACTION_STOP + ctx.startService(intent) + } + } + } +} diff --git a/package.json b/package.json index 34cb2d8c0..b0b3c3206 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "description": "Golos Blogs — децентрализованная платформа блогов, работающая на блокчейне Golos.", "main": "dist/electron/electron.js", "scripts": { + "cordova": "cordova", "build-version": "./server/build-version.sh", "build-hash": "node check_integrity --save", "build": "npm run build-hash && NODE_ENV=production NODE_CONFIG_ENV=production,blacklist ./node_modules/.bin/webpack --config ./webpack/prod.config.js", @@ -20,9 +21,12 @@ "start:local": "./.env.start.local.sh", "storybook": "start-storybook -p 6006", "build-storybook": "build-storybook", - "dev:app": "cross-env NODE_CONFIG_ENV=blacklist IS_APP=1 ./node_modules/@babel/node/bin/babel-node.js ./webpack/dev-server.js", + "dev:app": "npm run build-hash && cross-env NODE_CONFIG_ENV=blacklist IS_APP=1 ./node_modules/@babel/node/bin/babel-node.js ./webpack/dev-server.js", "build:app": "npm run build-hash && cross-env NODE_ENV=production NODE_CONFIG_ENV=production,desktop ./node_modules/.bin/webpack --config ./webpack/prod-app.config.js", - "build:app-entry": "cross-env NODE_ENV=production NODE_CONFIG_ENV=production,desktop ./node_modules/@babel/node/bin/babel-node.js build_app_entry.js" + "build:app-entry": "cross-env NODE_ENV=production NODE_CONFIG_ENV=production,desktop ./node_modules/@babel/node/bin/babel-node.js build_app_entry.js", + "prebuild:mobile": "cross-env NODE_CONFIG_ENV=production,mobile node ./prebuild_mobile.js", + "build:mobile": "npm run build-hash && cross-env NODE_ENV=production NODE_CONFIG_ENV=production,mobile ./node_modules/.bin/webpack --config ./webpack/prod-mobile.config.js", + "postbuild:mobile": "npm run build:app-entry dist _mobile && cross-env NODE_CONFIG_ENV=production,mobile node ./postbuild_mobile.js && cd cordova && cordova prepare && cordova run android" }, "author": "Golos ", "license": "MIT", @@ -138,11 +142,23 @@ "@babel/plugin-transform-runtime": "^7.16.0", "@babel/preset-env": "^7.16.0", "@babel/preset-react": "^7.16.0", + "@red-mobile/cordova-plugin-shortcuts-android": "^1.0.1", "babel-eslint": "^10.1.0", "babel-loader": "^8.2.3", "babel-plugin-styled-components": "^1.5.1", "chai": "^4.1.2", + "cheerio": "1.0.0-rc.10", "co-mocha": "^1.2.2", + "cordova-android": "^10.1.2", + "cordova-config": "^0.7.0", + "cordova-plugin-advanced-http": "^3.3.1", + "cordova-plugin-androidx-adapter": "^1.1.3", + "cordova-plugin-backbutton": "^0.3.0", + "cordova-plugin-badge": "^0.8.9", + "cordova-plugin-device": "^3.0.0", + "cordova-plugin-file": "^7.0.0", + "cordova-plugin-native-logs": "^1.0.5", + "cordova-plugin-splashscreen": "^6.0.2", "core-js": "^3.19.1", "cross-env": "^7.0.3", "eslint": "^4.9.0", @@ -151,6 +167,7 @@ "eslint-plugin-jsx-a11y": "^6.0.2", "eslint-plugin-react": "^7.4.0", "folder-hash": "^4.0.4", + "gls-blogs-native-core": "file:native_core", "jsdom": "^21.0.0", "mini-css-extract-plugin": "^0.4.0", "mocha": "^5.2.0", @@ -162,7 +179,7 @@ "svg-sprite-loader": "^4.3.0", "uglifyjs-webpack-plugin": "^1.2.5", "webpack-command": "^0.2.1", - "webpack-merge": "^4.1.2", + "webpack-merge": "5.2.0", "webpack-serve": "^1.0.4" }, "optionalDependencies": { @@ -177,5 +194,23 @@ "tabWidth": 4, "semi": true, "trailingComma": "es5" + }, + "cordova": { + "platforms": [ + "android" + ], + "plugins": { + "cordova-plugin-advanced-http": { + "ANDROIDBLACKLISTSECURESOCKETPROTOCOLS": "SSLv3,TLSv1" + }, + "cordova-plugin-androidx-adapter": {}, + "cordova-plugin-backbutton": {}, + "cordova-plugin-native-logs": {}, + "cordova-plugin-splashscreen": {}, + "@red-mobile/cordova-plugin-shortcuts-android": { + "ANDROIDX_CORE_VERSION": "1.3.2" + }, + "gls-blogs-native-core": {} + } } } diff --git a/postbuild_mobile.js b/postbuild_mobile.js new file mode 100644 index 000000000..d8f44e398 --- /dev/null +++ b/postbuild_mobile.js @@ -0,0 +1,89 @@ +const fs = require('fs') +const fse = require('fs-extra') +const cheerio = require('cheerio') +const CordovaConfig = require('cordova-config') +const config = require('config') + +function dirExists(path) { + try { + return fs.statSync(path).isDirectory(); + } catch (e) { + return false + } +} + +function copyDir(dir) { + fse.copySync(dir, distPath + '/' + dir) +} + +function patchLocalPluginVersion(moduleName, dir) { // Patch plugin version to do not broke on cordova prepare + const coreVersion = JSON.parse(fs.readFileSync(dir + '/package.json')).version + console.log(' ' + moduleName, 'version:', coreVersion) + + let json = JSON.parse(fs.readFileSync(distPath + '/package.json')) + if (!json.devDependencies[moduleName]) { + throw new Error(moduleName + ' dependency is not found in package.json/devDependencies') + } + json.devDependencies[moduleName] = coreVersion + json = JSON.stringify(json, null, 2) + fs.writeFileSync(distPath + '/package.json', json) +} + + +const distPath = 'cordova' +const configFile = distPath + '/config.xml' +const indexHtml = distPath + '/www/index.html' + +console.log('--- Copying files to "' + distPath + '" folder...') + +if (!fs.existsSync(distPath)) { + fs.mkdirSync(distPath) +} +fs.copyFileSync('config.xml', configFile) +fs.copyFileSync('package.json', distPath + '/package.json') // for "cordova prepare" +fs.copyFileSync('yarn.lock', distPath + '/yarn.lock') + +console.log('--- Adding hostname to "' + configFile + '" file...') + +let cc = new CordovaConfig(configFile) +cc.setPreference('hostname', config.get('mobile.site_domain')) +cc.writeSync() + +console.log('--- Moving react "build" folder to "' + distPath + '/www"') + +if (dirExists('dist')) { + fs.rmSync(distPath + '/www', { recursive: true, force: true }); + fs.renameSync('dist', distPath + '/www') +} + +console.log('--- Copying "native_core" folder to "' + distPath + '/native_core"') + +copyDir('native_core') + +console.log('--- Patching "native_core" version for correct installation') + +patchLocalPluginVersion('gls-blogs-native-core', 'native_core') + +console.log('--- Clearing cordova in order to update in on "cordova prepare"') + +fs.rmSync(distPath + '/platforms', { recursive: true, force: true }) +fs.rmSync(distPath + '/plugins', { recursive: true, force: true }) + +console.log('--- Copying "res" folder to "' + distPath + '/res"') + +copyDir('res') + +console.log('--- Including cordova.js script into "' + indexHtml + '" file...') + +let idx = fs.readFileSync(indexHtml, 'utf8') +idx = cheerio.load(idx) +if (idx('script[src="cordova.js"]').length === 0) { + idx('').insertBefore('script:first-of-type') + idx('').insertBefore('script:first-of-type') + console.log('Included.') +} else { + console.log('Already exists.') +} +fs.writeFileSync(indexHtml, idx.html()) + +console.log('--- Copied. Installing Cordova plugins and platforms ("cordova prepare"), and building+running android') diff --git a/prebuild_mobile.js b/prebuild_mobile.js new file mode 100644 index 000000000..149890c0f --- /dev/null +++ b/prebuild_mobile.js @@ -0,0 +1,27 @@ +const config = require('config') +const fs = require('fs') +const app_version = require('./package.json').version + +console.log('--- Making default config for react build...') + +let cfg = {} +const copyKey = (key) => { + cfg[key] = config.get('mobile.' + key) +} +cfg.app_version = app_version +copyKey('ws_connection_app') +copyKey('ws_connection_exchange') +copyKey('logo') +copyKey('images') +copyKey('wallet_service') +copyKey('messenger_service') +copyKey('auth_service') +copyKey('notify_service') +copyKey('elastic_search') +copyKey('apidex_service') +copyKey('app_updater') +copyKey('forums') +copyKey('hidden_assets') +fs.writeFileSync('app/app_cfg.js', '/* Only Mobile. Generated automatically. Do not edit. */\r\nmodule.exports = ' + JSON.stringify(cfg, null, 4)) + +console.log('--- Config done. Next stage is running react build.') diff --git a/res/blg_setting.png b/res/blg_setting.png new file mode 100644 index 000000000..acaff94a8 Binary files /dev/null and b/res/blg_setting.png differ diff --git a/res/blog0.png b/res/blog0.png new file mode 100644 index 000000000..d465a76eb Binary files /dev/null and b/res/blog0.png differ diff --git a/res/blog1.png b/res/blog1.png new file mode 100644 index 000000000..3ded20e91 Binary files /dev/null and b/res/blog1.png differ diff --git a/res/icon-hdpi.png b/res/icon-hdpi.png new file mode 100644 index 000000000..2483cf19c Binary files /dev/null and b/res/icon-hdpi.png differ diff --git a/res/icon-ldpi.png b/res/icon-ldpi.png new file mode 100644 index 000000000..ec2818048 Binary files /dev/null and b/res/icon-ldpi.png differ diff --git a/res/icon-mdpi.png b/res/icon-mdpi.png new file mode 100644 index 000000000..0d15b2e6c Binary files /dev/null and b/res/icon-mdpi.png differ diff --git a/res/icon-xhdpi.png b/res/icon-xhdpi.png new file mode 100644 index 000000000..07a750362 Binary files /dev/null and b/res/icon-xhdpi.png differ diff --git a/res/icon-xxhdpi.png b/res/icon-xxhdpi.png new file mode 100644 index 000000000..98833859e Binary files /dev/null and b/res/icon-xxhdpi.png differ diff --git a/res/icon-xxxhdpi.png b/res/icon-xxxhdpi.png new file mode 100644 index 000000000..ba5d3c762 Binary files /dev/null and b/res/icon-xxxhdpi.png differ diff --git a/res/logo.png b/res/logo.png new file mode 100644 index 000000000..c1579ea6f Binary files /dev/null and b/res/logo.png differ diff --git a/res/logo_x.png b/res/logo_x.png new file mode 100644 index 000000000..78e852dd5 Binary files /dev/null and b/res/logo_x.png differ diff --git a/res/screen/android/land/splash-land-hdpi.png b/res/screen/android/land/splash-land-hdpi.png new file mode 100644 index 000000000..b5893cff3 Binary files /dev/null and b/res/screen/android/land/splash-land-hdpi.png differ diff --git a/res/screen/android/land/splash-land-ldpi.png b/res/screen/android/land/splash-land-ldpi.png new file mode 100644 index 000000000..05c53b4f2 Binary files /dev/null and b/res/screen/android/land/splash-land-ldpi.png differ diff --git a/res/screen/android/land/splash-land-mdpi.png b/res/screen/android/land/splash-land-mdpi.png new file mode 100644 index 000000000..604ba2a6f Binary files /dev/null and b/res/screen/android/land/splash-land-mdpi.png differ diff --git a/res/screen/android/land/splash-land-xhdpi.png b/res/screen/android/land/splash-land-xhdpi.png new file mode 100644 index 000000000..db95d626a Binary files /dev/null and b/res/screen/android/land/splash-land-xhdpi.png differ diff --git a/res/screen/android/land/splash-land-xxhdpi.png b/res/screen/android/land/splash-land-xxhdpi.png new file mode 100644 index 000000000..46146a12b Binary files /dev/null and b/res/screen/android/land/splash-land-xxhdpi.png differ diff --git a/res/screen/android/land/splash-land-xxxhdpi.png b/res/screen/android/land/splash-land-xxxhdpi.png new file mode 100644 index 000000000..9e4f263ad Binary files /dev/null and b/res/screen/android/land/splash-land-xxxhdpi.png differ diff --git a/res/screen/android/splash-hdpi.png b/res/screen/android/splash-hdpi.png new file mode 100644 index 000000000..e4593654f Binary files /dev/null and b/res/screen/android/splash-hdpi.png differ diff --git a/res/screen/android/splash-ldpi.png b/res/screen/android/splash-ldpi.png new file mode 100644 index 000000000..46d1e4f5b Binary files /dev/null and b/res/screen/android/splash-ldpi.png differ diff --git a/res/screen/android/splash-mdpi.png b/res/screen/android/splash-mdpi.png new file mode 100644 index 000000000..487b5560a Binary files /dev/null and b/res/screen/android/splash-mdpi.png differ diff --git a/res/screen/android/splash-xhdpi.png b/res/screen/android/splash-xhdpi.png new file mode 100644 index 000000000..41bcb64d2 Binary files /dev/null and b/res/screen/android/splash-xhdpi.png differ diff --git a/res/screen/android/splash-xxhdpi.png b/res/screen/android/splash-xxhdpi.png new file mode 100644 index 000000000..454380548 Binary files /dev/null and b/res/screen/android/splash-xxhdpi.png differ diff --git a/res/screen/android/splash-xxxhdpi.png b/res/screen/android/splash-xxxhdpi.png new file mode 100644 index 000000000..10e592473 Binary files /dev/null and b/res/screen/android/splash-xxxhdpi.png differ diff --git a/server/index.js b/server/index.js index 39d079411..de26fa3ff 100755 --- a/server/index.js +++ b/server/index.js @@ -52,6 +52,7 @@ global.$STM_Config = { forums: config.get('forums'), blocked_users, blocked_posts, + filter_apps: config.get('filter_apps'), authorization_required: config.has('authorization_required') && config.get('authorization_required'), ui_version: version || '1.0-unknown', }; diff --git a/webpack/dev-app.config.js b/webpack/dev-app.config.js index e3e2e7d12..5828b4d2f 100644 --- a/webpack/dev-app.config.js +++ b/webpack/dev-app.config.js @@ -1,5 +1,5 @@ const webpack = require('webpack'); -const merge = require('webpack-merge'); +const { merge } = require('webpack-merge'); const git = require('git-rev-sync'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const baseConfig = require('./base.config'); diff --git a/webpack/dev.config.js b/webpack/dev.config.js index 2606bb4aa..ef0d1c48a 100644 --- a/webpack/dev.config.js +++ b/webpack/dev.config.js @@ -1,5 +1,5 @@ const webpack = require('webpack'); -const merge = require('webpack-merge'); +const { merge } = require('webpack-merge') const git = require('git-rev-sync'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const baseConfig = require('./base.config'); diff --git a/webpack/prod-app.config.js b/webpack/prod-app.config.js index 9bce3b673..8c585d945 100644 --- a/webpack/prod-app.config.js +++ b/webpack/prod-app.config.js @@ -1,5 +1,5 @@ const webpack = require('webpack'); -const merge = require('webpack-merge'); +const { merge } = require('webpack-merge') const path = require('path'); let prodConfig = require('./prod.config'); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); diff --git a/webpack/prod-mobile.config.js b/webpack/prod-mobile.config.js new file mode 100644 index 000000000..1e7cdbd5d --- /dev/null +++ b/webpack/prod-mobile.config.js @@ -0,0 +1,45 @@ +const webpack = require('webpack'); +const { mergeWithCustomize, unique } = require('webpack-merge') +const path = require('path'); +let prodConfig = require('./prod.config'); +const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); + +delete prodConfig.optimization.minimizer + +module.exports = mergeWithCustomize({ + customizeArray: unique( + 'plugins', + ['DefinePlugin'], + (plugin) => plugin.constructor && plugin.constructor.name, + ), +})(prodConfig, { + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + BROWSER: JSON.stringify(true), + NODE_ENV: JSON.stringify('production'), + IS_APP: JSON.stringify(true), + MOBILE_APP: JSON.stringify(true), + }, + global: { + TYPED_ARRAY_SUPPORT: JSON.stringify(false), + }, + }), + ], + entry: { + app: [ './app/MainApp.js' ], + // vendor: ['react', 'react-dom', 'react-router'] + }, + output: { + path: path.resolve(__dirname, '../dist/assets'), + }, + optimization: { + minimizer: [ + new OptimizeCSSAssetsPlugin({ + cssProcessorOptions: { + safe: true, + } + }), + ], + }, +}); diff --git a/webpack/prod.config.js b/webpack/prod.config.js index b757e9a71..3226861f5 100644 --- a/webpack/prod.config.js +++ b/webpack/prod.config.js @@ -1,5 +1,5 @@ const webpack = require('webpack'); -const merge = require('webpack-merge'); +const { merge } = require('webpack-merge') const baseConfig = require('./base.config'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); diff --git a/yarn.lock b/yarn.lock index c55f11db1..b22e6a257 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1287,11 +1287,41 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@netflix/nerror@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@netflix/nerror/-/nerror-1.1.3.tgz#9d88eccca442f1d544f2761d15ea557dc0a44ed2" + integrity sha512-b+MGNyP9/LXkapreJzNUzcvuzZslj/RGgdVVJ16P2wSlYatfLycPObImqVJSmNAdyeShvNeM/pl3sVZsObFueg== + dependencies: + assert-plus "^1.0.0" + extsprintf "^1.4.0" + lodash "^4.17.15" + "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": version "2.1.8-no-fsevents.3" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b" integrity sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ== +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + "@npmcli/fs@^1.0.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" @@ -1329,6 +1359,11 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353" integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q== +"@red-mobile/cordova-plugin-shortcuts-android@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@red-mobile/cordova-plugin-shortcuts-android/-/cordova-plugin-shortcuts-android-1.0.1.tgz#5ac176fe47d9be979a313f53692db6a56d28dc1a" + integrity sha512-WlmgkCyp6WCTV/Y/AW9xg8gQrnxwGD8CxH90uWglHops8Uf0vsSnWpvxrlGQTjpOSavnqosgrnge58kkhd7ccw== + "@shellscape/koa-send@^4.1.0": version "4.1.3" resolved "https://registry.yarnpkg.com/@shellscape/koa-send/-/koa-send-4.1.3.tgz#1a7c8df21f63487e060b7bfd8ed82e1d3c4ae0b0" @@ -1614,6 +1649,11 @@ resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.6.tgz#8a1524eb5bd5e965c1e3735476f0262469f71440" integrity sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg== +"@xmldom/xmldom@^0.8.8": + version "0.8.10" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" + integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -1775,6 +1815,13 @@ alphanum-sort@^1.0.0: resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= +android-versions@^1.7.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/android-versions/-/android-versions-1.9.0.tgz#433d53fc6ed5ba2b8d3c2801cb5da3964013274d" + integrity sha512-13O2B6PQMEM4ej9n13ePRQeckrCoKbZrvuzlLvK+9s2QmncpHDbYzZxhgapN32sJNoifN6VAHexLnd/6CYrs7Q== + dependencies: + semver "^7.5.2" + ansi-align@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" @@ -1815,6 +1862,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/ansi/-/ansi-0.3.1.tgz#0c42d4fb17160d5a9af1e484bace1c66922c1b21" + integrity sha512-iFY7JCgHbepc0b82yLaw4IMortylNb6wG4kL+4R0C3iv6i+RHGHux/yUX5BTiRvSX/shMnngjR1YyNMnXEFh5A== + any-promise@^1.0.0, any-promise@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -1998,6 +2050,11 @@ asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + atob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.1.tgz#ae2d5a729477f289d60dd7f96a6314a22dd6c22a" @@ -2155,7 +2212,7 @@ base-x@^3.0.2: dependencies: safe-buffer "^5.0.1" -base64-js@^1.0.2, base64-js@^1.3.1: +base64-js@^1.0.2, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -2178,6 +2235,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +big-integer@^1.6.44: + version "1.6.52" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== + big.js@^3.1.3: version "3.2.0" resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" @@ -2237,6 +2299,13 @@ boxen@^1.2.1: term-size "^1.2.0" widest-line "^2.0.0" +bplist-parser@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.2.0.tgz#43a9d183e5bf9d545200ceac3e712f79ebbe8d0e" + integrity sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw== + dependencies: + big-integer "^1.6.44" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -2266,6 +2335,13 @@ braces@^2.2.2, braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -2707,6 +2783,30 @@ check-error@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" +cheerio-select@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.6.0.tgz#489f36604112c722afa147dedd0d4609c09e1696" + integrity sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g== + dependencies: + css-select "^4.3.0" + css-what "^6.0.1" + domelementtype "^2.2.0" + domhandler "^4.3.1" + domutils "^2.8.0" + +cheerio@1.0.0-rc.10: + version "1.0.0-rc.10" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.10.tgz#2ba3dcdfcc26e7956fc1f440e61d51c643379f3e" + integrity sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw== + dependencies: + cheerio-select "^1.5.0" + dom-serializer "^1.3.2" + domhandler "^4.2.0" + htmlparser2 "^6.1.0" + parse5 "^6.0.1" + parse5-htmlparser2-tree-adapter "^6.0.1" + tslib "^2.2.0" + chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -3175,6 +3275,95 @@ copy-to-clipboard@^3.3.1: dependencies: toggle-selection "^1.0.6" +cordova-android@^10.1.2: + version "10.1.2" + resolved "https://registry.yarnpkg.com/cordova-android/-/cordova-android-10.1.2.tgz#3abfabb5fbc77dc3b90d7173c64cb2f8ad3d80df" + integrity sha512-F28+NvgKO4ZhKFkqctCOh62mhVoNyUuRQh/F/nqp+Sti4ODv2rUa6UeW18khhdYTjlDeihHQsPqxvB7mI6fVYA== + dependencies: + android-versions "^1.7.0" + cordova-common "^4.0.2" + execa "^5.1.1" + fast-glob "^3.2.7" + fs-extra "^10.0.0" + is-path-inside "^3.0.3" + nopt "^5.0.0" + properties-parser "^0.3.1" + semver "^7.3.5" + untildify "^4.0.0" + which "^2.0.2" + +cordova-common@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cordova-common/-/cordova-common-4.1.0.tgz#06058ea00e9dd0c635b6884617a0df81b3d89359" + integrity sha512-sYfOSfpYGQOmUDlsARUbpT/EvVKT/E+GI3zwTXt+C6DjZ7xs6ZQVHs3umHKSidjf9yVM2LLmvGFpGrGX7aGxug== + dependencies: + "@netflix/nerror" "^1.1.3" + ansi "^0.3.1" + bplist-parser "^0.2.0" + cross-spawn "^7.0.1" + elementtree "^0.1.7" + endent "^1.4.1" + fast-glob "^3.2.2" + fs-extra "^9.0.0" + glob "^7.1.6" + plist "^3.0.1" + q "^1.5.1" + read-chunk "^3.2.0" + strip-bom "^4.0.0" + underscore "^1.9.2" + +cordova-config@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/cordova-config/-/cordova-config-0.7.0.tgz#a1e6d2cacaa4c3c5e821840932832fe44e5448fb" + integrity sha512-3ZIXl0qAZypaQevZ2BUUDqrSF9I+mnikuO5e641s29KEKDhDIQMq4v/iPNoV0/fxOYgeCqPNnjwJFx4w+oGSQA== + dependencies: + elementtree "^0.1.6" + pify "^2.3.0" + pinkie-promise "^1.0.0" + +cordova-plugin-advanced-http@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/cordova-plugin-advanced-http/-/cordova-plugin-advanced-http-3.3.1.tgz#903143a9aae3577cdbb6953fbe482902983b9237" + integrity sha512-hESuB3mxIHCUrzb5lm7juda6PSNcC5N8Invizj5wGV2rSldCapiNxMTEpzKR1UVPDDP2XOtBzO0SAYS+3+g/ig== + +cordova-plugin-androidx-adapter@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/cordova-plugin-androidx-adapter/-/cordova-plugin-androidx-adapter-1.1.3.tgz#aa7e673ee342de208a6a34a50986ee2ac4b5ba60" + integrity sha512-W1SImn0cCCvOSTSfWWp5TnanIQrSuh2Bch+dcZXIzEn0km3Qb7VryeAqHhgBQYwwzC5Ollk1DtUAk/AJSojuZA== + dependencies: + q "^1.5.1" + recursive-readdir "^2.2.2" + +cordova-plugin-backbutton@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/cordova-plugin-backbutton/-/cordova-plugin-backbutton-0.3.0.tgz#752fd68fff83d3917ae1eff6de2cd50fcce024e3" + integrity sha512-vPQQ8SZk2LcNP89sFVnZkFN3GChERSaxO1UlS2O+a86qUJZpzBgA+21C978wo/w43tk1+JDbzEw+PTIbYcex5w== + +cordova-plugin-badge@^0.8.9: + version "0.8.9" + resolved "https://registry.yarnpkg.com/cordova-plugin-badge/-/cordova-plugin-badge-0.8.9.tgz#c3b91e12ce18a51e42ede46763978583ed6c6d55" + integrity sha512-oVv+z3B7Jgi7/gnE/jvSnjBsBNwZVoLmJYwTUw3nLmp50fnY22H7NWthTachEdle2EljDk8AzvhuDpdf0triIw== + +cordova-plugin-device@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cordova-plugin-device/-/cordova-plugin-device-3.0.0.tgz#e70d6def715629f62b96c1e0ee5a1299e96b1341" + integrity sha512-g8fFYOvleeYpklWvHwZ/T8/IzJe/3O0MGVDIUoqBru4v8SNDAbNVD3oOqoOQANBWGFQMg7GIkAAl8errCHZ7zQ== + +cordova-plugin-file@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cordova-plugin-file/-/cordova-plugin-file-7.0.0.tgz#40e72468e09439c402c81ff063d6ee74fffd763a" + integrity sha512-mSwy9GE5pHq2ZHhu/wYk/VhrwR5VLk+XQsk3+IiiFmDgcPsrVIyELkM2FZKX09cC6i+bJVTFVKUlwteSStj3ow== + +cordova-plugin-native-logs@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/cordova-plugin-native-logs/-/cordova-plugin-native-logs-1.0.5.tgz#43665b44de2f613d381bdc9352d53e4edd0890cb" + integrity sha512-X+l78oqw1U+eZoBF1nikRSa1vUlWw8QzNRPQWQ4VHFfjPUsfC0uvoDr2Spgn0tuUe+0kJ0ovRNeVpgWWb5LN2A== + +cordova-plugin-splashscreen@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/cordova-plugin-splashscreen/-/cordova-plugin-splashscreen-6.0.2.tgz#2151ee7256fbd64f07a84951854687855c56856f" + integrity sha512-7JiUfnInir+SCOEgTJ+5/cHF3UFl69jp6cAQfHtJaaQt9Pli8D8yTJjU0HGlJCvryvsVs4Xlc7/sEJM7vLJgvg== + core-js-compat@^3.18.0, core-js-compat@^3.19.0: version "3.19.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.19.1.tgz#fe598f1a9bf37310d77c3813968e9f7c7bb99476" @@ -3440,6 +3629,17 @@ css-select@^4.1.3: domutils "^2.6.0" nth-check "^2.0.0" +css-select@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== + dependencies: + boolbase "^1.0.0" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" + nth-check "^2.0.1" + css-selector-tokenizer@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86" @@ -3483,6 +3683,11 @@ css-what@^5.0.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad" integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg== +css-what@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + cssesc@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" @@ -3730,6 +3935,11 @@ decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== + deep-eql@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" @@ -3947,6 +4157,15 @@ dom-serializer@^1.0.1: domhandler "^4.2.0" entities "^2.0.0" +dom-serializer@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" @@ -3988,6 +4207,13 @@ domhandler@^4.0.0, domhandler@^4.2.0: dependencies: domelementtype "^2.2.0" +domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + domready@1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/domready/-/domready-1.0.8.tgz#91f252e597b65af77e745ae24dd0185d5e26d58c" @@ -4010,6 +4236,15 @@ domutils@^2.5.2, domutils@^2.6.0: domelementtype "^2.2.0" domhandler "^4.2.0" +domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + dont-sniff-mimetype@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz#5932890dc9f4e2f19e5eb02a20026e5e5efc8f58" @@ -4129,6 +4364,13 @@ electron-to-chromium@^1.3.896: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.898.tgz#0bd4090bf7c7003cb9bd31c4223a9f6aa1aab9dc" integrity sha512-dxEsaHy9Ter268LO7P8uWomuChbyML4zZk5F9+UZSozFRS7ggC5cQ8fPIM8Pec+6uWGdujuDagQhIbqjohUK2w== +elementtree@^0.1.6, elementtree@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/elementtree/-/elementtree-0.1.7.tgz#9ac91be6e52fb6e6244c4e54a4ac3ed8ae8e29c0" + integrity sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg== + dependencies: + sax "1.1.4" + elliptic@^6.0.0: version "6.4.0" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" @@ -4177,6 +4419,15 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" +endent@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/endent/-/endent-1.4.1.tgz#c58cc13dfc432d0b2c7faf74c13ffdca60b2d1c8" + integrity sha512-buHTb5c8AC9NshtP6dgmNLYkiT+olskbq1z6cEGvfGCF3Qphbu/1zz5Xu+yjTDln8RbxNhPoUyJ5H8MSrp1olQ== + dependencies: + dedent "^0.7.0" + fast-json-parse "^1.0.3" + objectorarray "^1.0.4" + enhanced-resolve@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.0.0.tgz#e34a6eaa790f62fccd71d93959f56b2b432db10a" @@ -4581,6 +4832,21 @@ execa@^0.8.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + exenv@^1.2.0: version "1.2.2" resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" @@ -4648,6 +4914,11 @@ extsprintf@^1.2.0: version "1.4.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" +extsprintf@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== + fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" @@ -4661,6 +4932,22 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-glob@^3.2.2, fast-glob@^3.2.7: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-parse@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fast-json-parse/-/fast-json-parse-1.0.3.tgz#43e5c61ee4efa9265633046b770fb682a7577c4d" + integrity sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw== + fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" @@ -4673,6 +4960,13 @@ fastparse@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + fbjs-css-vars@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8" @@ -4750,6 +5044,13 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + find-cache-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" @@ -4949,6 +5250,15 @@ fs-extra@^0.30.0: path-is-absolute "^1.0.0" rimraf "^2.2.8" +fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" @@ -4958,6 +5268,16 @@ fs-extra@^10.0.1: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-minipass@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" @@ -5106,6 +5426,11 @@ get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-symbol-description@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" @@ -5140,7 +5465,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@~5.1.0, glob-parent@~5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -5170,6 +5495,18 @@ glob@^7.1.3, glob@^7.1.4, glob@~7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.6: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^8.0.1: version "8.1.0" resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" @@ -5211,6 +5548,9 @@ globule@^1.0.0: lodash "^4.17.21" minimatch "~3.0.2" +"gls-blogs-native-core@file:native_core": + version "1.0.0" + golos-dex-lib-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/golos-dex-lib-js/-/golos-dex-lib-js-1.0.2.tgz#e0fe5d29781da8d0830ccb6bcfa5386bc73a2c5d" @@ -5706,6 +6046,11 @@ https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: agent-base "6" debug "4" +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -6222,6 +6567,11 @@ is-path-inside@^1.0.0: dependencies: path-is-inside "^1.0.1" +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-plain-obj@^1.1, is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -6289,6 +6639,11 @@ is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + is-string@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f" @@ -7295,6 +7650,16 @@ merge-options@1.0.1, merge-options@^1.0.0, merge-options@^1.0.1: dependencies: is-plain-obj "^1.1" +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + methods@^1.0.1, methods@~1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -7336,6 +7701,14 @@ micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" +micromatch@^4.0.4: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -7365,6 +7738,11 @@ mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" @@ -7391,6 +7769,13 @@ minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@^3.0.5, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + minimatch@^5.0.1, minimatch@~5.1.2: version "5.1.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" @@ -8004,6 +8389,13 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + npmlog@^4.0.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" @@ -8037,6 +8429,13 @@ nth-check@^2.0.0: dependencies: boolbase "^1.0.0" +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" @@ -8171,6 +8570,11 @@ object.values@^1.1.0: define-properties "^1.1.3" es-abstract "^1.19.1" +objectorarray@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/objectorarray/-/objectorarray-1.0.5.tgz#2c05248bbefabd8f43ad13b41085951aac5e68a5" + integrity sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg== + on-finished@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -8189,6 +8593,13 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + only@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" @@ -8296,7 +8707,7 @@ p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" -p-try@^2.0.0: +p-try@^2.0.0, p-try@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== @@ -8371,10 +8782,22 @@ parse-srcset@^1.0.2: resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE= +parse5-htmlparser2-tree-adapter@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" + integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== + dependencies: + parse5 "^6.0.1" + parse5@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" +parse5@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + parse5@^7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" @@ -8431,7 +8854,7 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= -path-key@^3.1.0: +path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -8496,12 +8919,12 @@ picomatch@^2.0.4, picomatch@^2.2.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== -picomatch@^2.3.0: +picomatch@^2.3.0, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pify@^2.0.0: +pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -8514,12 +8937,24 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== +pinkie-promise@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-1.0.0.tgz#d1da67f5482563bb7cf57f286ae2822ecfbf3670" + integrity sha512-5mvtVNse2Ml9zpFKkWBpGsTPwm3DKhs+c95prO/F6E7d6DN0FPqxs6LONpLNpyD7Iheb7QN4BbUoKJgo+DnkQA== + dependencies: + pinkie "^1.0.0" + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" dependencies: pinkie "^2.0.0" +pinkie@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-1.0.0.tgz#5a47f28ba1015d0201bda7bf0f358e47bec8c7e4" + integrity sha512-VFVaU1ysKakao68ktZm76PIdOhvEfoNNRaGkyLln9Os7r0/MCxqHjHyBM7dT3pgTiBybqiPtpqKfpENwdBp50Q== + pinkie@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" @@ -8561,6 +8996,15 @@ platform@1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.5.tgz#fb6958c696e07e2918d2eeda0f0bc9448d733444" +plist@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9" + integrity sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ== + dependencies: + "@xmldom/xmldom" "^0.8.8" + base64-js "^1.5.1" + xmlbuilder "^15.1.1" + plur@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/plur/-/plur-3.0.1.tgz#268652d605f816699b42b86248de73c9acd06a7c" @@ -9107,6 +9551,13 @@ prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" +properties-parser@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/properties-parser/-/properties-parser-0.3.1.tgz#1316e9539ffbfd93845e369b211022abd478771a" + integrity sha512-AkSQxQAviJ89x4FIxOyHGfO3uund0gvYo7lfD0E+Gp7gFQKrTNgtoYQklu8EhrfHVZUzTwKGZx2r/KDSfnljcA== + dependencies: + string.prototype.codepointat "^0.2.0" + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -9169,7 +9620,7 @@ punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" -q@^1.1.2: +q@^1.1.2, q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= @@ -9208,6 +9659,11 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + quick-lru@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" @@ -9520,6 +9976,14 @@ react@^18.2.0: dependencies: loose-envify "^1.1.0" +read-chunk@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-3.2.0.tgz#2984afe78ca9bfbbdb74b19387bf9e86289c16ca" + integrity sha512-CEjy9LCzhmD7nUpJ1oVOE6s/hBkejlcJEgLQHVnQznOSilOPb+kpKktlLfFDK3/WP43+F80xkUTM2VOkYoSYvQ== + dependencies: + pify "^4.0.1" + with-open-file "^0.1.6" + read-pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" @@ -9629,6 +10093,13 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" +recursive-readdir@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" + integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== + dependencies: + minimatch "^3.0.5" + redent@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" @@ -9968,6 +10439,11 @@ retry@^0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + rgb-regex@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" @@ -10022,6 +10498,13 @@ run-async@^2.2.0: dependencies: is-promise "^2.1.0" +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" @@ -10098,6 +10581,11 @@ sass-loader@6.0.6: lodash.tail "^4.1.1" pify "^3.0.0" +sax@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.4.tgz#74b6d33c9ae1e001510f179a91168588f1aedaa9" + integrity sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg== + sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -10211,6 +10699,11 @@ semver@^7.3.4, semver@^7.3.5: dependencies: lru-cache "^6.0.0" +semver@^7.5.2: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + sentence-case@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-1.1.3.tgz#8034aafc2145772d3abe1509aa42c9e1042dc139" @@ -10339,7 +10832,7 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" -signal-exit@^3.0.7: +signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -10680,6 +11173,11 @@ string-width@^1.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string.prototype.codepointat@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc" + integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg== + string.prototype.trimend@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" @@ -10732,10 +11230,20 @@ strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + strip-indent@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" @@ -11185,6 +11693,11 @@ tslib@^1.10.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.2.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tslib@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" @@ -11337,6 +11850,11 @@ underscore.string@^3.3.5: sprintf-js "^1.0.3" util-deprecate "^1.0.2" +underscore@^1.9.2: + version "1.13.7" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.7.tgz#970e33963af9a7dda228f17ebe8399e5fbe63a10" + integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -11449,6 +11967,11 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + unzip-response@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" @@ -11789,11 +12312,13 @@ webpack-log@^1.0.1, webpack-log@^1.1.1, webpack-log@^1.1.2: loglevelnext "^1.0.1" uuid "^3.1.0" -webpack-merge@^4.1.2: - version "4.1.3" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.3.tgz#8aaff2108a19c29849bc9ad2a7fd7fce68e87c4a" +webpack-merge@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.2.0.tgz#31cbcc954f8f89cd4b06ca8d97a38549f7f3f0c9" + integrity sha512-QBglJBg5+lItm3/Lopv8KDDK01+hjdg2azEwi/4vKJ8ZmGPdtJsTpjtNNOW3a4WiqzXdCATtTudOZJngE7RKkA== dependencies: - lodash "^4.17.5" + clone-deep "^4.0.1" + wildcard "^2.0.0" webpack-serve@^1.0.4: version "1.0.4" @@ -11986,10 +12511,24 @@ widest-line@^2.0.0: dependencies: string-width "^2.1.1" +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + window-size@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" +with-open-file@^0.1.6: + version "0.1.7" + resolved "https://registry.yarnpkg.com/with-open-file/-/with-open-file-0.1.7.tgz#e2de8d974e8a8ae6e58886be4fe8e7465b58a729" + integrity sha512-ecJS2/oHtESJ1t3ZfMI3B7KIDKyfN0O16miWxdn30zdh66Yd3LsRFebXZXq6GU4xfxLf6nVxp9kIqElb5fqczA== + dependencies: + p-finally "^1.0.0" + p-try "^2.1.0" + pify "^4.0.1" + wordwrap@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" @@ -12072,6 +12611,11 @@ xml-name-validator@^4.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== +xmlbuilder@^15.1.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"