Compare commits

27 Commits

Author SHA1 Message Date
Laura Hausmann 491fe19084 Fix local and public timeline links 2023-10-12 18:04:51 +02:00
Laura Hausmann 8a36343a64 Fix preload crossorigin 2023-10-12 17:48:29 +02:00
Laura Hausmann 11bd8feee7 Preload correct js files 2023-10-12 17:45:20 +02:00
Laura Hausmann 65325a63fb Update README 2023-10-09 22:48:03 +02:00
Laura Hausmann 2efd4a6a4e Add access_token to websocket connect 2023-10-09 22:37:08 +02:00
Laura Hausmann bf8ac44011 Move state generation to page load 2023-10-09 20:13:07 +02:00
Laura Hausmann 81844e476c Auth improvements 2023-10-09 19:31:31 +02:00
Laura Hausmann 5255fdd31c Actually disable button on click 2023-10-09 19:29:04 +02:00
Laura Hausmann 37bded2cf7 Reformat link footer data 2023-10-09 19:23:18 +02:00
Laura Hausmann 576b247913 Add display name to sidebar 2023-10-09 19:12:14 +02:00
Laura Hausmann 9b729bf1b1 Remove/disable user profile edit links 2023-10-09 19:06:32 +02:00
Laura Hausmann 56c94f16bf Remove/disable user preferences links 2023-10-09 19:02:26 +02:00
Laura Hausmann b6c52750a3 remove noop code 2023-10-09 18:51:23 +02:00
Laura Hausmann 88926ac162 Update README 2023-10-09 18:38:57 +02:00
Laura Hausmann 4e4757bac5 logout button 2023-10-09 18:26:53 +02:00
Laura Hausmann 58709501ca add mascot 2023-10-09 18:22:33 +02:00
Laura Hausmann 26164479ff don't send query 2023-10-09 18:18:34 +02:00
Laura Hausmann 923b7a73a2 attempt 1 2023-10-09 18:09:18 +02:00
Laura Hausmann 36202a5cc8 Add baseUrl state property 2023-10-09 15:45:56 +02:00
FloatingGhost 0a6462682a char limit 2023-04-14 16:20:35 +01:00
FloatingGhost 02869b6aed fix streaming for some streams 2023-04-14 16:14:27 +01:00
FloatingGhost ed237e4d0e Update akkoma branch 2023-04-14 16:02:11 +01:00
FloatingGhost cb792539c3 Merge remote-tracking branch 'glitch-soc/main' into akkoma 2023-04-14 15:19:19 +01:00
FloatingGhost 494f853b7c fix build script 2022-12-08 15:10:35 +00:00
FloatingGhost 702f82cb04 match fedibird's SW 2022-12-08 14:59:06 +00:00
FloatingGhost 2358754bb3 don't update all timelines 2022-12-08 14:50:38 +00:00
FloatingGhost 076f7534db Akkoma patches
don't replace my history

re-add CI

lint stuff

upload to $build-tag

change SW path

use default SW

fix notifications streams

use akkoma hashtag schema

add local intl
2022-12-08 14:32:45 +00:00
53 changed files with 676 additions and 178 deletions
+1
View File
@@ -6,6 +6,7 @@
# Ignore bundler config and downloaded libraries.
/.bundle
distribution
/vendor/bundle
# Ignore the default SQLite database.
+30
View File
@@ -0,0 +1,30 @@
pipeline:
build:
when:
event:
- tag
- push
image: node:16
commands:
- yarn
- TARGET=distribution ./build.sh
release:
when:
event:
- tag
- push
image: node:16
secrets:
- SCW_ACCESS_KEY
- SCW_SECRET_KEY
- SCW_DEFAULT_ORGANIZATION_ID
commands:
- apt-get update && apt-get install -y rclone wget zip
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
- chmod +x scaleway-cli
- ./scaleway-cli object config install type=rclone
- export BUILD_TAG=$${CI_COMMIT_TAG:-"$CI_COMMIT_BRANCH"}
- zip mastofe.zip -r distribution
- rclone copyto mastofe.zip scaleway:akkoma-updates/frontend/$BUILD_TAG/masto-fe.zip
+26 -9
View File
@@ -1,14 +1,31 @@
# Mastodon Glitch Edition
# Mastodon Glitch Edition (standalone frontend)
> Now with automated deploys!
This is a very hacky fork of akkoma-masto-fe that adds standalone support (meaning your browser can OAuth against an arbitrary instance). It's currently tested to "work" (login doesn't break, basic functionality works) with Iceshrimp and GoToSocial (and it obviously works with Mastodon).
[![Build Status](https://img.shields.io/circleci/project/github/glitch-soc/mastodon.svg)][circleci]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/glitch-soc/mastodon.svg)][code_climate]
To try this out, go to [masto-fe.iceshrimp.dev](https://masto-fe.iceshrimp.dev), type in your instance domain name (for split domain setups, use the web domain) & press the button.
[circleci]: https://circleci.com/gh/glitch-soc/mastodon
[code_climate]: https://codeclimate.com/github/glitch-soc/mastodon
To set this up yourself, clone the repo into e.g. `/home/user/masto-fe-standalone` and run `yarn && yarn build:production` (you might have to use `NODE_OPTIONS=--openssl-legacy-provider` until we've rebased this onto upstream glitch).
So here's the deal: we all work on this code, and anyone who uses that does so absolutely at their own risk. can you dig it?
Then configure nginx for a subdomain like this:
- You can view documentation for this project at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/).
- And contributing guidelines are available [here](CONTRIBUTING.md) and [here](https://glitch-soc.github.io/docs/contributing/).
```
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
include sites/example.com/inc/ssl.conf;
server_name masto.example.com;
location / {
root /home/user/masto-fe-standalone/public/;
index index.html;
try_files $uri /index.html;
}
}
```
And open `https://masto.example.com` in your browser, type in your instance domain, press the button & follow the OAuth flow.
Should anything break, open `https://masto.example.com/logout.html` or clear local storage manually.
@@ -874,7 +874,7 @@ export function changePinnedAccountsSuggestions(value) {
type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE,
value,
};
}
};
export function resetPinnedAccountsEditor() {
return {
@@ -18,7 +18,10 @@ const urlBase64ToUint8Array = (base64String) => {
return outputArray;
};
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
const getApplicationServerKey = () => {
const k = document.querySelector('[name="applicationServerKey"]');
return k === null ? '' : k.getAttribute('content');
};
const getRegistration = () => navigator.serviceWorker.ready;
@@ -21,7 +21,7 @@ export const fetchServer = () => (dispatch, getState) => {
dispatch(fetchServerRequest());
api(getState)
.get('/api/v2/instance').then(({ data }) => {
.get('/api/v1/instance').then(({ data }) => {
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
dispatch(fetchServerSuccess(data));
}).catch(err => dispatch(fetchServerFail(err)));
@@ -79,10 +79,21 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
}
},
shouldUpdate (timelineId, streamName) {
const stream = streamName[0];
if (timelineId === 'home' && streamName.startsWith('user')) {
return true;
}
return timelineId === stream;
},
onReceive (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
if ((timelineId === 'home' && data.stream[0].startsWith('user')) || (timelineId === 'community' && data.stream[0].startsWith('public')) || (timelineId === data.stream[0])) {
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
}
break;
case 'status.update':
dispatch(updateStatus(JSON.parse(data.payload)));
@@ -3,7 +3,7 @@ import { submitMarkers } from './markers';
import api, { getLinks } from 'flavours/glitch/api';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from 'flavours/glitch/compare_id';
import { me, usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
import { toServerSideType } from 'flavours/glitch/utils/filters';
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
@@ -153,7 +153,7 @@ export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) {
}
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, tagged, max_id: maxId });
@@ -171,7 +171,7 @@ export const expandHashtagTimeline = (hashtag, { maxId, tags, local } =
};
export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done);
export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia, allow_local_only: !!allowLocalOnly }, done);
export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia }, done);
export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done);
export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done);
+11
View File
@@ -51,6 +51,15 @@ const authorizationHeaderFromState = getState => {
};
};
/**
* @param {() => import('immutable').Map<string,any>} getState
* @returns string
*/
const baseUrlFromState = getState => {
const baseUrl = getState && getState().getIn(['meta', 'base_url'], '');
return `${baseUrl}`;
};
/**
* @param {() => import('immutable').Map} getState
* @returns {import('axios').AxiosInstance}
@@ -62,6 +71,8 @@ export default function api(getState) {
...authorizationHeaderFromState(getState),
},
baseURL: baseUrlFromState(getState),
transformResponse: [
function (data) {
try {
@@ -117,7 +117,7 @@ export default class ErrorBoundary extends React.PureComponent {
<FormattedMessage
id='web_app_crash.change_your_settings'
defaultMessage='Change your {settings}'
values={{ settings: <a href={preferencesLink}><FormattedMessage id='web_app_crash.settings' defaultMessage='settings' /></a> }}
values={{ settings: <span><FormattedMessage id='web_app_crash.settings' defaultMessage='settings' /></span> }}
/>
</li>
)}
@@ -55,8 +55,9 @@ export const ImmutableHashtag = ({ hashtag }) => (
name={hashtag.get('name')}
href={hashtag.get('url')}
to={`/tags/${hashtag.get('name')}`}
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
people={0}
uses={0}
history={[]}
/>
);
@@ -10,6 +10,7 @@ const messages = defineMessages({
unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
direct: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
local: { id: 'privacy.local.short', defaultMessage: 'Local users only' },
});
class VisibilityIcon extends ImmutablePureComponent {
@@ -28,6 +29,7 @@ class VisibilityIcon extends ImmutablePureComponent {
unlisted: 'unlock',
private: 'lock',
direct: 'envelope',
local: 'lock',
}[visibility];
const label = intl.formatMessage(messages[visibility]);
@@ -36,7 +36,7 @@ const createIdentityContext = state => ({
accountId: state.meta.me,
disabledAccountId: state.meta.disabled_account_id,
accessToken: state.meta.access_token,
permissions: state.role ? state.role.permissions : 0,
permissions: [],
});
export default class Mastodon extends React.PureComponent {
@@ -38,8 +38,6 @@ class ActionBar extends React.PureComponent {
let menu = [];
menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink });
menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink });
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
@@ -21,6 +21,8 @@ import { length } from 'stringz';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
missingDescriptionConfirm: { id: 'confirmations.missing_media_description.confirm',
defaultMessage: 'Send anyway' },
missingDescriptionMessage: {
id: 'confirmations.missing_media_description.message',
defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.',
@@ -236,29 +238,29 @@ class ComposeForm extends ImmutablePureComponent {
} = this.props;
let selectionEnd, selectionStart;
// Caret/selection handling.
if (focusDate !== prevProps.focusDate) {
switch (true) {
case preselectDate !== prevProps.preselectDate && this.props.isInReply && preselectOnReply:
selectionStart = text.search(/\s/) + 1;
selectionEnd = text.length;
break;
case !isNaN(caretPosition) && caretPosition !== null:
selectionStart = selectionEnd = caretPosition;
break;
default:
selectionStart = selectionEnd = text.length;
}
if (textarea) {
// Because of the wicg-inert polyfill, the activeElement may not be
// immediately selectable, we have to wait for observers to run, as
// described in https://github.com/WICG/inert#performance-and-gotchas
Promise.resolve().then(() => {
textarea.setSelectionRange(selectionStart, selectionEnd);
textarea.focus();
if (!singleColumn) textarea.scrollIntoView();
}).catch(console.error);
}
// Caret/selection handling.
if (focusDate !== prevProps.focusDate) {
switch (true) {
case preselectDate !== prevProps.preselectDate && this.props.isInReply && preselectOnReply:
selectionStart = text.search(/\s/) + 1;
selectionEnd = text.length;
break;
case !isNaN(caretPosition) && caretPosition !== null:
selectionStart = selectionEnd = caretPosition;
break;
default:
selectionStart = selectionEnd = text.length;
}
if (textarea) {
// Because of the wicg-inert polyfill, the activeElement may not be
// immediately selectable, we have to wait for observers to run, as
// described in https://github.com/WICG/inert#performance-and-gotchas
Promise.resolve().then(() => {
textarea.setSelectionRange(selectionStart, selectionEnd);
textarea.focus();
if (!singleColumn) textarea.scrollIntoView();
}).catch(console.error);
}
// Refocuses the textarea after submitting.
} else if (textarea && prevProps.isSubmitting && !isSubmitting) {
@@ -306,13 +308,12 @@ class ComposeForm extends ImmutablePureComponent {
isEditing,
} = this.props;
const countText = this.getFulltextForCharacterCounting();
const countText = this.getFulltextForCharacterCounting();
return (
<div className='compose-form'>
<WarningContainer />
<ReplyIndicatorContainer />
<ReplyIndicatorContainer />
<div className={`spoiler-input ${spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
<AutosuggestInput
@@ -374,18 +375,18 @@ class ComposeForm extends ImmutablePureComponent {
</div>
</div>
<Publisher
countText={countText}
disabled={!this.canSubmit()}
isEditing={isEditing}
onSecondarySubmit={handleSecondarySubmit}
onSubmit={handleSubmit}
privacy={privacy}
sideArm={sideArm}
/>
</div>
);
}
<Publisher
countText={countText}
disabled={!this.canSubmit()}
isEditing={isEditing}
onSecondarySubmit={handleSecondarySubmit}
onSubmit={handleSubmit}
privacy={privacy}
sideArm={sideArm}
/>
</div>
);
}
}
@@ -130,8 +130,7 @@ class Header extends ImmutablePureComponent {
><Icon id='sign-out' /></a>
</nav>
);
}
};
}
export default injectIntl(Header);
@@ -24,16 +24,10 @@ export default class NavigationBar extends ImmutablePureComponent {
</Permalink>
<div className='navigation-bar__profile'>
<div>{this.props.account.get('display_name')}</div>
<Permalink className='acct' href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}>
<strong>@{this.props.account.get('acct')}</strong>
</Permalink>
{ profileLink !== undefined && (
<a
className='edit'
href={profileLink}
><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
)}
</div>
<div className='navigation-bar__actions'>
@@ -42,5 +36,4 @@ export default class NavigationBar extends ImmutablePureComponent {
</div>
);
}
}
@@ -94,7 +94,6 @@ class Publisher extends ImmutablePureComponent {
</div>
);
}
}
export default injectIntl(Publisher);
@@ -136,7 +136,6 @@ class SearchResults extends ImmutablePureComponent {
</div>
);
}
}
export default injectIntl(SearchResults);
@@ -39,7 +39,7 @@ const mapStateToProps = state => ({
const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => {
if (needsLockWarning) {
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href={profileLink}><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <span><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></span> }} />} />;
}
if (hashtagWarning) {
@@ -71,15 +71,8 @@ class LocalSettingsNavigation extends React.PureComponent {
/>
<LocalSettingsNavigationItem
active={index === 5}
href={preferencesLink}
index={5}
icon='cog'
title={intl.formatMessage(messages.preferences)}
/>
<LocalSettingsNavigationItem
active={index === 6}
className='close'
index={6}
index={5}
onNavigate={onClose}
icon='times'
title={intl.formatMessage(messages.close)}
@@ -299,12 +299,12 @@ class LocalSettingsPage extends React.PureComponent {
defaultMessage="This setting is now controlled from Mastodon's {settings_page_link}"
values={{
settings_page_link: (
<a href={preferenceLink('user_setting_expand_spoilers')}>
<span>
<FormattedMessage
id='settings.shared_settings_link'
defaultMessage='user preferences'
/>
</a>
</span>
),
}}
/>
@@ -78,12 +78,12 @@ class PublicTimeline extends React.PureComponent {
};
componentDidMount () {
const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props;
const { dispatch, onlyMedia, onlyRemote } = this.props;
const { signedIn } = this.context.identity;
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote, allowLocalOnly }));
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
if (signedIn) {
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly }));
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
}
}
@@ -91,16 +91,16 @@ class PublicTimeline extends React.PureComponent {
const { signedIn } = this.context.identity;
if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote || prevProps.allowLocalOnly !== this.props.allowLocalOnly) {
const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props;
const { dispatch, onlyMedia, onlyRemote } = this.props;
if (this.disconnect) {
this.disconnect();
}
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote, allowLocalOnly }));
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
if (signedIn) {
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly }));
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
}
}
}
@@ -117,9 +117,9 @@ class PublicTimeline extends React.PureComponent {
};
handleLoadMore = maxId => {
const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props;
const { dispatch, onlyMedia, onlyRemote } = this.props;
dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote, allowLocalOnly }));
dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote }));
};
render () {
@@ -0,0 +1,100 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { expandPublicTimeline, expandCommunityTimeline } from 'flavours/glitch/actions/timelines';
import Masonry from 'react-masonry-infinite';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import DetailedStatusContainer from 'flavours/glitch/features/status/containers/detailed_status_container';
import { debounce } from 'lodash';
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
const mapStateToProps = (state, { local }) => {
const timeline = state.getIn(['timelines', local ? 'community' : 'public'], ImmutableMap());
return {
statusIds: timeline.get('items', ImmutableList()),
isLoading: timeline.get('isLoading', false),
hasMore: timeline.get('hasMore', false),
};
};
export default @connect(mapStateToProps)
class PublicTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool.isRequired,
hasMore: PropTypes.bool.isRequired,
local: PropTypes.bool,
};
componentDidMount () {
this._connect();
}
componentDidUpdate (prevProps) {
if (prevProps.local !== this.props.local) {
this._disconnect();
this._connect();
}
}
_connect () {
const { dispatch, local } = this.props;
dispatch(local ? expandCommunityTimeline() : expandPublicTimeline());
}
handleLoadMore = () => {
const { dispatch, statusIds, local } = this.props;
const maxId = statusIds.last();
if (maxId) {
dispatch(local ? expandCommunityTimeline({ maxId }) : expandPublicTimeline({ maxId }));
}
}
setRef = c => {
this.masonry = c;
}
handleHeightChange = debounce(() => {
if (!this.masonry) {
return;
}
this.masonry.forcePack();
}, 50)
render () {
const { statusIds, hasMore, isLoading } = this.props;
const sizes = [
{ columns: 1, gutter: 0 },
{ mq: '415px', columns: 1, gutter: 10 },
{ mq: '640px', columns: 2, gutter: 10 },
{ mq: '960px', columns: 3, gutter: 10 },
{ mq: '1255px', columns: 3, gutter: 10 },
];
const loader = (isLoading && statusIds.isEmpty()) ? <LoadingIndicator key={0} /> : undefined;
return (
<Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}>
{statusIds.map(statusId => (
<div className='statuses-grid__item' key={statusId}>
<DetailedStatusContainer
id={statusId}
compact
measureHeight
onHeightChange={this.handleHeightChange}
/>
</div>
)).toArray()}
</Masonry>
);
}
}
@@ -65,7 +65,7 @@ class DeprecatedSettingsModal extends React.PureComponent {
<ul>
{ settings.map((setting_name) => (
<li>
<a href={preferenceLink(setting_name)}><FormattedMessage {...messages[setting_name]} /></a>
<span><FormattedMessage {...messages[setting_name]} /></span>
</li>
)) }
</ul>
@@ -55,42 +55,12 @@ class LinkFooter extends React.PureComponent {
return (
<div className='link-footer'>
<p>
<strong>{domain}</strong>:
{' '}
<Link to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
{statusPageUrl && (
<>
{DividingCircle}
<a href={statusPageUrl} target='_blank' rel='noopener'><FormattedMessage id='footer.status' defaultMessage='Status' /></a>
</>
)}
{canInvite && (
<>
{DividingCircle}
<a href='/invites' target='_blank'><FormattedMessage id='footer.invite' defaultMessage='Invite people' /></a>
</>
)}
{canProfileDirectory && (
<>
{DividingCircle}
<Link to='/directory'><FormattedMessage id='footer.directory' defaultMessage='Profiles directory' /></Link>
</>
)}
<strong>Masto-FE-standalone</strong>
{DividingCircle}
<Link to='/privacy-policy'><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
</p>
<p>
<strong>Mastodon</strong>:
{' '}
<a href='https://joinmastodon.org' target='_blank'><FormattedMessage id='footer.about' defaultMessage='About' /></a>
{DividingCircle}
<a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='footer.get_app' defaultMessage='Get the app' /></a>
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='Source code' /></a>
{DividingCircle}
<Link to='/keyboard-shortcuts'><FormattedMessage id='footer.keyboard_shortcuts' defaultMessage='Keyboard shortcuts' /></Link>
{DividingCircle}
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
{DividingCircle}
v{version}
</p>
</div>
@@ -85,7 +85,6 @@ class NavigationPanel extends React.Component {
<hr />
{!!preferencesLink && <ColumnLink transparent href={preferencesLink} icon='cog' text={intl.formatMessage(messages.preferences)} />}
<ColumnLink transparent onClick={onOpenSettings} icon='cogs' text={intl.formatMessage(messages.app_settings)} />
</React.Fragment>
)}
@@ -144,7 +144,7 @@ export const languages = initialState?.languages;
export const statusPageUrl = getMeta('status_page_url');
// Glitch-soc-specific settings
export const maxChars = (initialState && initialState.max_toot_chars) || 500;
export const maxChars = (initialState && initialState.char_limit) || 500;
export const favouriteModal = getMeta('favourite_modal');
export const pollLimits = (initialState && initialState.poll_limits);
export const defaultContentType = getMeta('default_content_type');
@@ -0,0 +1,13 @@
import { RULES_FETCH_SUCCESS } from 'flavours/glitch/actions/rules';
import { List as ImmutableList, fromJS } from 'immutable';
const initialState = ImmutableList();
export default function rules(state = initialState, action) {
switch (action.type) {
case RULES_FETCH_SUCCESS:
return state;
default:
return state;
}
}
+4 -20
View File
@@ -88,25 +88,7 @@ const sharedCallbacks = {
},
received (data) {
const { stream } = data;
subscriptions.filter(({ channelName, params }) => {
const streamChannelName = stream[0];
if (stream.length === 1) {
return channelName === streamChannelName;
}
const streamIdentifier = stream[1];
if (['hashtag', 'hashtag:local'].includes(channelName)) {
return channelName === streamChannelName && params.tag === streamIdentifier;
} else if (channelName === 'list') {
return channelName === streamChannelName && params.list === streamIdentifier;
}
return false;
}).forEach(subscription => {
subscriptions.forEach(subscription => {
subscription.onReceive(data);
});
},
@@ -230,7 +212,9 @@ const createConnection = (streamingAPIBaseURL, accessToken, channelName, { conne
channelName = params.shift();
if (streamingAPIBaseURL.startsWith('ws')) {
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken);
params.push(`access_token=${accessToken}`);
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming?stream=user&${params.join('&')}`, accessToken);
ws.onopen = connected;
ws.onmessage = e => received(JSON.parse(e.data));
@@ -0,0 +1,28 @@
import * as registerPushNotifications from 'flavours/glitch/actions/push_notifications';
import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications';
import { default as Mastodon, store } from 'flavours/glitch/containers/mastodon';
import React from 'react';
import ReactDOM from 'react-dom';
import ready from './ready';
const perf = require('./performance');
function main() {
perf.start('main()');
ready(() => {
const mountNode = document.getElementById('mastodon');
const props = JSON.parse(mountNode.getAttribute('data-props'));
ReactDOM.render(<Mastodon {...props} />, mountNode);
store.dispatch(setupBrowserNotifications());
if (process.env.NODE_ENV === 'production') {
// avoid offline in dev mode because it's harder to debug
require('offline-plugin/runtime').install();
store.dispatch(registerPushNotifications.register());
}
perf.stop('main()');
});
}
export default main;
@@ -1,6 +1,6 @@
export const preferencesLink = '/settings/preferences';
export const profileLink = '/settings/profile';
export const signOutLink = '/auth/sign_out';
export const preferencesLink = undefined;
export const profileLink = undefined;
export const signOutLink = '/logout.html';
export const privacyPolicyLink = '/privacy-policy';
export const accountAdminLink = (id) => `/admin/accounts/${id}`;
export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses/${status_id}`;
@@ -25,7 +25,7 @@ export const logOut = () => {
submitButton.setAttribute('type', 'submit');
form.appendChild(submitButton);
form.method = 'post';
form.method = 'get';
form.action = signOutLink;
form.style.display = 'none';
+1 -1
View File
@@ -21,7 +21,7 @@ export const fetchServer = () => (dispatch, getState) => {
dispatch(fetchServerRequest());
api(getState)
.get('/api/v2/instance').then(({ data }) => {
.get('/api/v1/instance').then(({ data }) => {
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
dispatch(fetchServerSuccess(data));
}).catch(err => dispatch(fetchServerFail(err)));
+11
View File
@@ -51,6 +51,15 @@ const authorizationHeaderFromState = getState => {
};
};
/**
* @param {() => import('immutable').Map<string,any>} getState
* @returns string
*/
const baseUrlFromState = getState => {
const baseUrl = getState && getState().getIn(['meta', 'base_url'], '');
return `${baseUrl}`;
};
/**
* @param {() => import('immutable').Map<string,any>} getState
* @returns {import('axios').AxiosInstance}
@@ -62,6 +71,8 @@ export default function api(getState) {
...authorizationHeaderFromState(getState),
},
baseURL: baseUrlFromState(getState),
transformResponse: [
function (data) {
try {
@@ -32,7 +32,7 @@ const createIdentityContext = state => ({
accountId: state.meta.me,
disabledAccountId: state.meta.disabled_account_id,
accessToken: state.meta.access_token,
permissions: state.role ? state.role.permissions : 0,
permissions: [],
});
export default class Mastodon extends React.PureComponent {
+1 -1
View File
@@ -273,7 +273,7 @@
"footer.invite": "Invite people",
"footer.keyboard_shortcuts": "Keyboard shortcuts",
"footer.privacy_policy": "Privacy policy",
"footer.source_code": "View source code",
"footer.source_code": "Source code",
"footer.status": "Status",
"generic.saved": "Saved",
"getting_started.heading": "Getting started",
@@ -11,7 +11,7 @@ function openWebCache() {
}
function fetchRoot() {
return fetch('/', { credentials: 'include', redirect: 'manual' });
return fetch('/web', { credentials: 'include', redirect: 'manual' });
}
precacheAndRoute(self.__WB_MANIFEST);
@@ -58,7 +58,7 @@ registerRoute(
// Cause a new version of a registered Service Worker to replace an existing one
// that is already installed, and replace the currently active worker on open pages.
self.addEventListener('install', function(event) {
event.waitUntil(Promise.all([openWebCache(), fetchRoot()]).then(([cache, root]) => cache.put('/', root)));
event.waitUntil(Promise.all([openWebCache(), fetchRoot()]).then(([cache, root]) => cache.put('/web', root)));
});
self.addEventListener('activate', function(event) {
@@ -68,20 +68,8 @@ self.addEventListener('activate', function(event) {
self.addEventListener('fetch', function(event) {
const url = new URL(event.request.url);
if (url.pathname === '/auth/sign_out') {
const asyncResponse = fetch(event.request);
const asyncCache = openWebCache();
event.respondWith(asyncResponse.then(response => {
if (response.ok || response.type === 'opaqueredirect') {
return Promise.all([
asyncCache.then(cache => cache.delete('/')),
indexedDB.deleteDatabase('mastodon'),
]).then(() => response);
}
return response;
}));
if (url.pathname.startsWith('/web')) {
return;
}
});
+3 -1
View File
@@ -236,8 +236,10 @@ const createConnection = (streamingAPIBaseURL, accessToken, channelName, { conne
channelName = params.shift();
if (streamingAPIBaseURL.startsWith('ws')) {
params.push(`access_token=${accessToken}`);
// @ts-expect-error
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken);
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming?${params.join('&')}`, accessToken);
// @ts-expect-error
ws.onopen = connected;
+2 -2
View File
@@ -24,8 +24,8 @@ export const logOut = () => {
submitButton.setAttribute('type', 'submit');
form.appendChild(submitButton);
form.method = 'post';
form.action = '/auth/sign_out';
form.method = 'get';
form.action = '/logout.html';
form.style.display = 'none';
document.body.appendChild(form);
Executable
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
TARGET="${TARGET:-./distribution}" # Where pleromas repository is sitting
mkdir -p $TARGET/emoji
die() {
echo "Die: $@"
exit 1
}
[ -d "${TARGET}" ] || die "${TARGET} directory is missing, are you sure TARGET is set to a pleroma repository? (Info: TARGET=${TARGET} )"
yarn install -D || die "Installing dependencies via yarn failed"
rm -rf public/packs public/assets
env -i "PATH=$PATH" npm run build || die "Building the frontend failed"
cp public/packs/sw.js "${TARGET}/sw.js" || die "installing sw.js (service-worker) failed"
rm -rf "${TARGET}/packs" || die "Removing old assets in priv/static/packs failed"
cp -r public/packs "${TARGET}/packs" || die "Copying new assets in priv/static/packs failed"
rm -rf "${TARGET}/emoji/*.svg" || die "Removing the old emoji assets failed"
cp -r public/emoji/* "${TARGET}/emoji" || die "Installing the new emoji assets failed"
+2 -4
View File
@@ -58,8 +58,7 @@ module.exports = {
entry: entries,
output: {
filename: 'js/[name]-[chunkhash].js',
chunkFilename: 'js/[name]-[chunkhash].chunk.js',
filename: 'js/[name].js',
hotUpdateChunkFilename: 'js/[id]-[hash].hot-update.js',
hashFunction: 'sha256',
path: output.path,
@@ -102,8 +101,7 @@ module.exports = {
},
),
new MiniCssExtractPlugin({
filename: 'css/[name]-[contenthash:8].css',
chunkFilename: 'css/[name]-[contenthash:8].chunk.css',
filename: 'css/[name].css',
}),
new AssetsManifestPlugin({
integrity: true,
Executable
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
TARGET="${TARGET:-./distribution}" # Where pleromas repository is sitting
mkdir -p $TARGET/emoji
die() {
echo "Die: $@"
exit 1
}
[ -d "${TARGET}" ] || die "${TARGET} directory is missing, are you sure TARGET is set to a pleroma repository? (Info: TARGET=${TARGET} )"
yarn install -D || die "Installing dependencies via yarn failed"
rm -rf public/packs public/assets
env -i "PATH=$PATH" npm run build:development || die "Building the frontend failed"
rm -rf "${TARGET}/packs" || die "Removing old assets in priv/static/packs failed"
cp -r public/packs "${TARGET}/packs" || die "Copying new assets in priv/static/packs failed"
rm -rf "${TARGET}/emoji/*.svg" || die "Removing the old emoji assets failed"
+5 -2
View File
@@ -1,14 +1,17 @@
{
"name": "@mastodon/mastodon",
"description": "mastodon",
"license": "AGPL-3.0-or-later",
"engines": {
"node": ">=14"
},
"scripts": {
"postversion": "git push --tags",
"build:development": "cross-env RAILS_ENV=development NODE_ENV=development ./bin/webpack",
"build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack",
"build:development": "cross-env NODE_ENV=development webpack --config config/webpack/development.js",
"build:production": "cross-env NODE_ENV=production webpack --config config/webpack/production.js",
"build": "cross-env NODE_ENV=production webpack --config config/webpack/production.js",
"manage:translations": "node ./config/webpack/translationRunner.js",
"dev": "cross-env NODE_ENV=development webpack-dev-server --config config/webpack/development.js --progress --color",
"start": "node ./streaming/index.js",
"test": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:typecheck && ${npm_execpath} run test:jest",
"test:lint": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:lint:sass",
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
TARGET="${TARGET:-./distribution}"
rm -rf "${TARGET}/packs" || die "Removing old assets in priv/static/packs failed"
cp -r public/packs "${TARGET}/packs" || die "Copying new assets in priv/static/packs failed"
rm -rf "${TARGET}/emoji/*.svg" || die "Removing the old emoji assets failed"
cp -r public/emoji/* "${TARGET}/emoji" || die "Installing the new emoji assets failed"
+101
View File
@@ -0,0 +1,101 @@
document.addEventListener("DOMContentLoaded", async function() {
await ready();
});
async function ready() {
const domain = localStorage.getItem('domain');
let accessToken = localStorage.getItem(`access_token`);
if (domain) document.getElementById('instance').value = domain;
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
if (domain && code && !accessToken) await getToken(code, domain).then(res => accessToken = res);
if (accessToken) {
window.location.href = '/prepare.html';
}
}
async function auth() {
setMessage('Please wait');
const instance = document.getElementById('instance').value;
const domain = instance.match(/(?:https?:\/\/)?(.*)/)[1];
if (!domain) {
setMessage('Invalid instance', false);
return;
}
localStorage.setItem('domain', domain);
// We need to run this every time in cases like Iceshrimp, where the client id/secret aren't reusable (yet) because they contain use-once session information
await registerApp(domain);
authorize(domain);
}
async function registerApp(domain) {
setMessage('Registering app');
const appsUrl = `https://${domain}/api/v1/apps`;
const formData = new FormData();
formData.append('client_name', 'Masto-FE standalone');
formData.append('redirect_uris', document.location.origin + document.location.pathname);
formData.append('scopes', 'read write follow push');
// eslint-disable-next-line promise/catch-or-return
await fetch(appsUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(formData),
})
.then(async res => {
const app = await res.json();
localStorage.setItem(`client_id`, app.client_id);
localStorage.setItem(`client_secret`, app.client_secret);
});
}
function authorize(domain) {
setMessage('Authorizing');
const clientId = localStorage.getItem(`client_id`);
document.location.href = `https://${domain}/oauth/authorize?response_type=code&client_id=${clientId}&redirect_uri=${document.location.origin + document.location.pathname}&scope=read+write+follow+push`;
}
async function getToken(code, domain) {
setMessage('Getting token');
const tokenUrl = `https://${domain}/oauth/token`;
const clientId = localStorage.getItem(`client_id`);
const clientSecret = localStorage.getItem(`client_secret`);
const formData = new FormData();
formData.append('grant_type', 'authorization_code');
formData.append('code', code);
formData.append('client_id', clientId);
formData.append('client_secret', clientSecret);
formData.append('scope', 'read write follow push');
formData.append('redirect_uri', document.location.origin + document.location.pathname);
// eslint-disable-next-line promise/catch-or-return
return fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(formData),
})
.then(async res => {
const app = await res.json();
if (app.access_token) localStorage.setItem(`access_token`, app.access_token);
return app.access_token;
});
}
function setMessage(message, disabled = true) {
document.getElementById('message').textContent = message;
document.getElementById('btn').disabled = disabled;
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

+33
View File
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta content='width=device-width, initial-scale=1' name='viewport'>
<title>Masto-FE standalone</title>
<link rel="manifest" type="applicaton/manifest+json" href="/manifest.json" />
<meta name="theme-color" content="#282c37" />
<script crossorigin='anonymous' src="/packs/js/locales.js"></script>
<script crossorigin='anonymous' src="/packs/js/locales/glitch/en.js"></script>
<link rel='preload' as='script' href='/packs/js/flavours/glitch/async/getting_started.js'>
<link rel='preload' as='script' href='/packs/js/flavours/glitch/async/compose.js'>
<link rel='preload' as='script' href='/packs/js/flavours/glitch/async/home_timeline.js'>
<link rel='preload' as='script' href='/packs/js/flavours/glitch/async/notifications.js'>
<script id='initial-state' type='application/json'>{}</script>
<script src="/verify-state.js"></script>
<script src="/packs/js/core/common.js"></script>
<link rel="stylesheet" media="all" href="/packs/css/core/common.css" />
<script src="/packs/js/flavours/glitch/common.js"></script>
<link rel="stylesheet" media="all" href="/packs/css/flavours/glitch/common.css" />
<script src="/packs/js/flavours/glitch/home.js"></script>
</head>
<body class='app-body no-reduce-motion system-font'>
<div class='app-holder' data-props='{&quot;locale&quot;:&quot;en&quot;}' id='mastodon'>
</div>
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login | Masto-FE standalone</title>
<script src="/auth.js"></script>
</head>
<body>
<input type="text" id="instance" placeholder="yourinstance.tld">
<button onclick="auth()" id="btn">Log in</button>
<span id="message"></span>
</body>
</html>
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Logout | Masto-FE standalone</title>
<script>
localStorage.clear();
window.location.href = "/login.html";
</script>
</head>
<body>
Clearing local storage and redirecting back to <a href="/login.html">login</a>...
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
{
"background_color": "#191b22",
"categories": ["social"],
"description": "Masto-FE standalone",
"display": "standalone",
"name": "Masto-FE standalone",
"serviceworker": {
"src": "/sw.js"
},
"start_url": "/getting-started",
"theme_color": "#282c37"
}
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login | Masto-FE standalone</title>
<script src="/verify-state.js"></script>
</head>
<body>
<p>Preparing state object...</p>
</body>
</html>
+103
View File
@@ -0,0 +1,103 @@
loadState().then(_ => null);
async function loadState() {
const domain = localStorage.getItem('domain');
const access_token = localStorage.getItem('access_token');
const storedState = localStorage.getItem('initial_state');
if (!domain || !access_token) {
window.location.href = '/login.html';
return;
}
if (storedState && window.location.pathname !== '/prepare.html') {
document.getElementById('initial-state').textContent = storedState;
}
const apiUrl = `https://${domain}/api`;
const instance = await fetch(`${apiUrl}/v1/instance`).then(async p => await p.json());
const options = {headers: {Authorization: `Bearer ${access_token}`}};
const credentials = await fetch(`${apiUrl}/v1/accounts/verify_credentials`, options).then(async p => await p.json());
const state = {
"accounts": {
"plc":{
"accepts_direct_messages_from":"everybody",
"acct": credentials.acct,
"avatar": credentials.avatar,
"avatar_static": credentials.avatar_static,
"bot": credentials.bot,
"created_at": credentials.created_at,
"display_name": credentials.display_name,
"emojis":[],
"fields":[],
"follow_requests_count":0,
"followers_count": credentials.followers_count,
"following_count": credentials.following_count,
"fqn":`${credentials.acct}@${domain}`,
"header": credentials.header,
"header_static": credentials.header_static,
"id": credentials.id,
"last_status_at": credentials.created_at,
"locked": credentials.locked,
"note":"",
"source": credentials.source,
"statuses_count": credentials.statuses_count,
"url": credentials.url,
"username": credentials.acct
}
},
"char_limit": instance.configuration.statuses.max_characters,
"compose": {
"allow_content_types": [
"text/x.misskeymarkdown"
],
"default_privacy": credentials.source.privacy,
"default_sensitive": credentials.source.sensitive,
"me": credentials.id
},
"media_attachments": {
"accept_content_types": instance.configuration.media_attachments.supported_mime_types
},
"meta": {
"access_token": access_token,
"admin": "0",
"advanced_layout": true,
"auto_play_gif": false,
"boost_modal": false,
"compact_reaction": false,
"delete_modal": true,
"display_sensitive_media": false,
"domain": domain,
"enable_reaction": true,
"locale": "en",
"mascot": "/images/mascot.svg",
"max_toot_chars": instance.configuration.statuses.max_characters,
"me": credentials.id,
"reduce_motion": false,
"show_quote_button": true,
"base_url": `https://${domain}`,
"streaming_api_base_url": `wss://${domain}`,
"title": `${instance.title}`,
"unfollow_modal": true,
"source_url": 'https://iceshrimp.dev/iceshrimp/masto-fe-standalone',
"version": instance.version
},
"poll_limits": {
"max_expiration": instance.configuration.polls.max_expiration,
"max_option_chars": instance.configuration.polls.max_characters_per_option,
"max_options": instance.configuration.polls.max_options,
"min_expiration": instance.configuration.polls.min_expiration
},
"push_subscription": null,
"rights": {
"admin": false,
"delete_others_notice": false
},
"settings": {}
};
const json = JSON.stringify(state);
if (window.location.pathname !== '/prepare.html') document.getElementById('initial-state').textContent = json;
localStorage.setItem("initial_state", json);
if (window.location.pathname === '/prepare.html') window.location.href = '/';
}