Compare commits
No commits in common. "master" and "master" have entirely different histories.
|
@ -1,9 +0,0 @@
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
end_of_line = lf
|
|
||||||
charset = utf-8
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
|
@ -1,12 +1,10 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/__sapper__
|
.sapper
|
||||||
|
yarn.lock
|
||||||
|
templates/.*
|
||||||
|
assets/*.css
|
||||||
/mastodon
|
/mastodon
|
||||||
/mastodon.log
|
mastodon.log
|
||||||
/src/template.html
|
assets/robots.txt
|
||||||
/static/*.css
|
/inline-script-checksum.json
|
||||||
/static/robots.txt
|
|
||||||
/static/inline-script.js.map
|
|
||||||
/static/emoji-mart-all.json
|
|
||||||
/src/inline-script/checksum.js
|
|
||||||
yarn-error.log
|
|
||||||
|
|
75
.travis.yml
|
@ -1,53 +1,55 @@
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- "10"
|
- "8"
|
||||||
dist: trusty # needed for chrome headless
|
dist: trusty # needed for chrome headless
|
||||||
sudo: required # needed for various sudo operations
|
sudo: required # needed for chrome headless
|
||||||
addons:
|
addons:
|
||||||
chrome: stable
|
chrome: stable
|
||||||
postgresql: "10"
|
postgresql: "10"
|
||||||
apt:
|
apt:
|
||||||
packages:
|
packages:
|
||||||
- autoconf
|
|
||||||
- bison
|
|
||||||
- build-essential
|
|
||||||
- file
|
|
||||||
- g++
|
|
||||||
- gcc
|
|
||||||
- imagemagick
|
|
||||||
- libffi-dev
|
|
||||||
- libgdbm-dev
|
|
||||||
- libgdbm3
|
|
||||||
- libicu-dev
|
|
||||||
- libidn11-dev
|
|
||||||
- libncurses5-dev
|
|
||||||
- libpq-dev
|
|
||||||
- libprotobuf-dev
|
|
||||||
- libreadline6-dev
|
|
||||||
- libssl-dev
|
|
||||||
- libxml2-dev
|
|
||||||
- libxslt1-dev
|
|
||||||
- libyaml-dev
|
|
||||||
- pkg-config nodejs
|
|
||||||
- postgresql-10
|
- postgresql-10
|
||||||
- postgresql-client-10
|
- postgresql-client-10
|
||||||
- postgresql-contrib-10
|
- postgresql-contrib-10
|
||||||
|
# the following are mastodon dependencies
|
||||||
|
- imagemagick
|
||||||
|
- libpq-dev
|
||||||
|
- libxml2-dev
|
||||||
|
- libxslt1-dev
|
||||||
|
- file
|
||||||
|
- g++
|
||||||
|
- libprotobuf-dev
|
||||||
- protobuf-compiler
|
- protobuf-compiler
|
||||||
- redis-server
|
- pkg-config nodejs
|
||||||
- redis-tools
|
- gcc
|
||||||
|
- autoconf
|
||||||
|
- bison
|
||||||
|
- build-essential
|
||||||
|
- libssl-dev
|
||||||
|
- libyaml-dev
|
||||||
|
- libreadline6-dev
|
||||||
- zlib1g-dev
|
- zlib1g-dev
|
||||||
|
- libncurses5-dev
|
||||||
|
- libffi-dev
|
||||||
|
- libgdbm3
|
||||||
|
- libgdbm-dev
|
||||||
|
- redis-tools
|
||||||
|
- libidn11-dev
|
||||||
|
- libicu-dev
|
||||||
|
services:
|
||||||
|
- redis-server
|
||||||
before_install:
|
before_install:
|
||||||
|
- npm install -g npm@5
|
||||||
- npm install -g greenkeeper-lockfile@1
|
- npm install -g greenkeeper-lockfile@1
|
||||||
# install yarn
|
|
||||||
- curl -o- -L https://yarnpkg.com/install.sh | bash -s
|
|
||||||
- export PATH="$HOME/.yarn/bin:$PATH"
|
|
||||||
- ./bin/setup-mastodon-in-travis.sh
|
- ./bin/setup-mastodon-in-travis.sh
|
||||||
before_script:
|
before_script:
|
||||||
- yarn run lint
|
- npm run lint
|
||||||
- greenkeeper-lockfile-update
|
- greenkeeper-lockfile-update
|
||||||
after_script:
|
after_script:
|
||||||
- greenkeeper-lockfile-upload
|
- greenkeeper-lockfile-upload
|
||||||
script: travis_retry yarn run $COMMAND
|
install:
|
||||||
|
- npm ci || npm i
|
||||||
|
script: travis_retry npm run $COMMAND
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- PGPORT=5433
|
- PGPORT=5433
|
||||||
|
@ -57,17 +59,16 @@ matrix:
|
||||||
include:
|
include:
|
||||||
- env: BROWSER=chrome:headless COMMAND=test-browser-suite0
|
- env: BROWSER=chrome:headless COMMAND=test-browser-suite0
|
||||||
- env: BROWSER=chrome:headless COMMAND=test-browser-suite1
|
- env: BROWSER=chrome:headless COMMAND=test-browser-suite1
|
||||||
- env: COMMAND=test-unit
|
- env: COMMAND=deploy-dev-travis
|
||||||
- env: COMMAND=deploy-all-travis
|
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- env: COMMAND=deploy-all-travis
|
- env: COMMAND=deploy-dev-travis
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
- /^greenkeeper/.*$/
|
- /^greenkeeper/.*$/
|
||||||
cache:
|
cache:
|
||||||
yarn: true
|
|
||||||
bundler: true
|
|
||||||
directories:
|
directories:
|
||||||
- /home/travis/.rvm/
|
- $HOME/.npm
|
||||||
- /home/travis/ffmpeg-static/
|
- $HOME/.rvm
|
||||||
|
- $HOME/.bundle
|
||||||
|
- $HOME/.yarn-cache
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
# Breaking changes
|
|
||||||
|
|
||||||
This document contains a list of _breaking changes_ for Pinafore. For a full changelog, see [the GitHub release page](https://github.com/nolanlawson/pinafore/releases).
|
|
||||||
|
|
||||||
## 1.0.0
|
|
||||||
|
|
||||||
**Breaking change:** This version [switches Pinafore from npm to yarn](https://github.com/nolanlawson/pinafore/pull/927). Those who self-host Pinafore will need to make the following changes:
|
|
||||||
|
|
||||||
1. [Install yarn](https://yarnpkg.com/en/docs/install) if you haven't already.
|
|
||||||
2. Instead of `npm install`, run `yarn --pure-lockfile`.
|
|
||||||
|
|
||||||
This change fixes [a functional bug in Webpack](https://github.com/nolanlawson/pinafore/pull/926). If you use npm instead of yarn, Pinafore may not build correctly.
|
|
||||||
|
|
||||||
### Notes:
|
|
||||||
|
|
||||||
- Using `yarn start` instead of `npm start` should not make a difference.
|
|
||||||
- Those using Docker shouldn't need to change anything.
|
|
182
CONTRIBUTING.md
|
@ -1,40 +1,41 @@
|
||||||
# Contributing to Pinafore
|
# Contributing to Pinafore
|
||||||
|
|
||||||
## Dev server
|
## Caveats
|
||||||
|
|
||||||
|
Please note that this project is _very_ beta right now, and I'm
|
||||||
|
not in a good position to accept large PRs for
|
||||||
|
big new features.
|
||||||
|
|
||||||
|
I'm making my code open-source for the sake of
|
||||||
|
transparency and because it's the right thing to do, but I'm hesitant
|
||||||
|
to start nurturing a community because of
|
||||||
|
[all that entails](https://nolanlawson.com/2017/03/05/what-it-feels-like-to-be-an-open-source-maintainer/).
|
||||||
|
|
||||||
|
So I may not be very responsive to PRs or issues. Thanks for understanding.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
To run a dev server with hot reloading:
|
To run a dev server with hot reloading:
|
||||||
|
|
||||||
yarn run dev
|
npm run dev
|
||||||
|
|
||||||
Now it's running at `localhost:4002`.
|
Now it's running at `localhost:4002`.
|
||||||
|
|
||||||
**Linux users:** for file changes to work,
|
|
||||||
you'll probably want to run `export CHOKIDAR_USEPOLLING=1`
|
|
||||||
because of [this issue](https://github.com/paulmillr/chokidar/issues/237).
|
|
||||||
|
|
||||||
## Linting
|
## Linting
|
||||||
|
|
||||||
Pinafore uses [JavaScript Standard Style](https://standardjs.com/).
|
Pinafore uses [JavaScript Standard Style](https://standardjs.com/).
|
||||||
|
|
||||||
Lint:
|
Lint:
|
||||||
|
|
||||||
yarn run lint
|
npm run lint
|
||||||
|
|
||||||
Automatically fix most linting issues:
|
Automatically fix most linting issues:
|
||||||
|
|
||||||
yarn run lint-fix
|
npm run lint-fix
|
||||||
|
|
||||||
## Integration tests
|
## Testing
|
||||||
|
|
||||||
Integration tests use [TestCafé](https://devexpress.github.io/testcafe/) and a live local Mastodon instance
|
Testing requires running Mastodon itself, meaning the [Mastodon development guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Development-guide.md) is relevant here. In particular, you'll need a recent version of Ruby, Redis, and Postgres running.
|
||||||
running on `localhost:3000`.
|
|
||||||
|
|
||||||
### Running integration tests
|
|
||||||
|
|
||||||
The integration tests require running Mastodon itself,
|
|
||||||
meaning the[Mastodon development guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Development-guide.md)
|
|
||||||
is relevant here. In particular, you'll need a recent
|
|
||||||
version of Ruby, Redis, and Postgres running. For a full list of deps, see `bin/setup-mastodon-in-travis.sh`.
|
|
||||||
|
|
||||||
Run integration tests, using headless Chrome by default:
|
Run integration tests, using headless Chrome by default:
|
||||||
|
|
||||||
|
@ -42,92 +43,44 @@ Run integration tests, using headless Chrome by default:
|
||||||
|
|
||||||
Run tests for a particular browser:
|
Run tests for a particular browser:
|
||||||
|
|
||||||
BROWSER=chrome yarn run test-browser
|
BROWSER=chrome npm run test-browser
|
||||||
BROWSER=chrome:headless yarn run test-browser
|
BROWSER=chrome:headless npm run test-browser
|
||||||
BROWSER=firefox yarn run test-browser
|
BROWSER=firefox npm run test-browser
|
||||||
BROWSER=firefox:headless yarn run test-browser
|
BROWSER=firefox:headless npm run test-browser
|
||||||
BROWSER=safari yarn run test-browser
|
BROWSER=safari npm run test-browser
|
||||||
BROWSER=edge yarn run test-browser
|
BROWSER=edge npm run test-browser
|
||||||
|
|
||||||
If the script isn't able to set up the Postgres database, try running:
|
## Testing in development mode
|
||||||
|
|
||||||
sudo su - postgres
|
|
||||||
|
|
||||||
Then:
|
|
||||||
|
|
||||||
psql -d template1 -c "CREATE USER pinafore WITH PASSWORD 'pinafore' CREATEDB;"
|
|
||||||
|
|
||||||
### Testing in development mode
|
|
||||||
|
|
||||||
In separate terminals:
|
In separate terminals:
|
||||||
|
|
||||||
1\. Run a Mastodon dev server:
|
1\. Run a Mastodon dev server:
|
||||||
|
|
||||||
yarn run run-mastodon
|
npm run run-mastodon
|
||||||
|
|
||||||
2\. Run a Pinafore dev server:
|
2\. Run a Pinafore dev server:
|
||||||
|
|
||||||
yarn run dev
|
npm run dev
|
||||||
|
|
||||||
3\. Run a debuggable TestCafé instance:
|
3\. Run a debuggable TestCafé instance:
|
||||||
|
|
||||||
npx testcafe --hostname localhost --skip-js-errors --debug-mode chrome tests/spec
|
npx testcafe --hostname localhost --skip-js-errors --debug-mode firefox tests/spec
|
||||||
|
|
||||||
### Test conventions
|
If you want to export the current data in the Mastodon instance as canned data,
|
||||||
|
so that it can be loaded later, run:
|
||||||
|
|
||||||
The tests have a naming convention:
|
npm run backup-mastodon-data
|
||||||
|
|
||||||
* `0xx-test-name.js`: tests that don't modify the Mastodon database (read-only)
|
## Writing tests
|
||||||
* `1xx-test-name.js`: tests that do modify the Mastodon database (read-write)
|
|
||||||
|
Tests use [TestCafé](https://devexpress.github.io/testcafe/). The tests have a naming convention:
|
||||||
|
|
||||||
|
* `0xx-test-name.js`: tests that don't modify the Mastodon database (post, delete, follow, etc.)
|
||||||
|
* `1xx-test-name.js`: tests that do modify the Mastodon database
|
||||||
|
|
||||||
In principle the `0-` tests don't have to worry about
|
In principle the `0-` tests don't have to worry about
|
||||||
clobbering each other, whereas the `1-` ones do.
|
clobbering each other, whereas the `1-` ones do.
|
||||||
|
|
||||||
### Mastodon used for testing
|
|
||||||
|
|
||||||
There are two parts to the Mastodon data used for testing:
|
|
||||||
|
|
||||||
1. A Postgres dump and a tgz containing the media files, located in `fixtures`
|
|
||||||
2. A script that populates the Mastodon backend with test data (`restore-mastodon-data.js`).
|
|
||||||
|
|
||||||
The reason we don't use a Postgres dump for everything
|
|
||||||
is that Mastodon will ignore changes made after a certain period of time, and we
|
|
||||||
don't want our tests to randomly start breaking one day. Running the script ensures that statuses,
|
|
||||||
favorites, boosts, etc. are all "fresh".
|
|
||||||
|
|
||||||
### Updating the test data
|
|
||||||
|
|
||||||
You probably don't want to do this, as the `0xx` tests are pretty rigidly defined against the test data.
|
|
||||||
Write a `1xx` test instead and insert what you need on-the-fly.
|
|
||||||
|
|
||||||
If you really need to, though, you can either:
|
|
||||||
|
|
||||||
1. Add new test data to `mastodon-data.js`
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
1. Comment out `await restoreMastodonData()` in `run-mastodon.js`
|
|
||||||
2. Make your changes manually to the live Mastodon
|
|
||||||
3. Run the steps in the next section to back it up to `fixtures/`
|
|
||||||
|
|
||||||
### Updating the Mastodon version
|
|
||||||
|
|
||||||
1. Run `rm -fr mastodon` to clear out all Mastodon data
|
|
||||||
1. Comment out `await restoreMastodonData()` in `run-mastodon.js` to avoid actually populating the database with statuses/favorites/etc.
|
|
||||||
2. Update the `GIT_TAG` in `run-mastodon.js` to whatever you want
|
|
||||||
3. Run `yarn run run-mastodon`
|
|
||||||
4. Run `yarn run backup-mastodon-data` to overwrite the data in `fixtures/`
|
|
||||||
5. Uncomment `await restoreMastodonData()` in `run-mastodon.js`
|
|
||||||
6. Commit all changed files
|
|
||||||
7. Run `rm -fr mastodon/` and `yarn run run-mastodon` to confirm everything's working
|
|
||||||
|
|
||||||
Check `mastodon.log` if you have any issues.
|
|
||||||
|
|
||||||
## Unit tests
|
|
||||||
|
|
||||||
There are also some unit tests that run in Node using Mocha. You can find them in `tests/unit` and
|
|
||||||
run them using `yarn run test-unit`.
|
|
||||||
|
|
||||||
## Debugging Webpack
|
## Debugging Webpack
|
||||||
|
|
||||||
The Webpack Bundle Analyzer `report.html` and `stats.json` are available publicly via e.g.:
|
The Webpack Bundle Analyzer `report.html` and `stats.json` are available publicly via e.g.:
|
||||||
|
@ -135,54 +88,17 @@ The Webpack Bundle Analyzer `report.html` and `stats.json` are available publicl
|
||||||
- [dev.pinafore.social/report.html](https://dev.pinafore.social/report.html)
|
- [dev.pinafore.social/report.html](https://dev.pinafore.social/report.html)
|
||||||
- [dev.pinafore.social/stats.json](https://dev.pinafore.social/stats.json)
|
- [dev.pinafore.social/stats.json](https://dev.pinafore.social/stats.json)
|
||||||
|
|
||||||
This is also available locally after `yarn run build` at `.sapper/client/report.html`.
|
This is also available locally after `npm run build` at `.sapper/client/report.html`.
|
||||||
|
|
||||||
## Codebase overview
|
## Updating Mastodon used for testing
|
||||||
|
|
||||||
Pinafore uses [SvelteJS](https://svelte.technology) and [SapperJS](https://sapper.svelte.technology). Most of it is a fairly typical Svelte/Sapper project, but there
|
1. Run `rm -fr mastodon` to clear out all Mastodon data
|
||||||
are some quirks, which are described below. This list of quirks is non-exhaustive.
|
1. Comment out `await restoreMastodonData()` in `run-mastodon.js` to avoid actually populating the database with statuses/favorites/etc.
|
||||||
|
2. Update the `GIT_TAG` in `run-mastodon.js` to whatever you want
|
||||||
|
3. Run `npm run run-mastodon`
|
||||||
|
4. Run `npm run backup-mastodon-data` to overwrite the data in `fixtures/`
|
||||||
|
5. Uncomment `await restoreMastodonData()` in `run-mastodon.js`
|
||||||
|
6. Commit all changed files
|
||||||
|
7. Run `rm -fr mastodon/` and `npm run run-mastodon` to confirm everything's working
|
||||||
|
|
||||||
### Prebuild process
|
Check `mastodon.log` if you have any issues.
|
||||||
|
|
||||||
The `template.html` is itself templated. The "template template" has some inline scripts, CSS, and SVGs
|
|
||||||
injected into it during the build process. SCSS is used for global CSS and themed CSS, but inside of the
|
|
||||||
components themselves, it's just vanilla CSS because I couldn't figure out how to get Svelte to run a SCSS
|
|
||||||
preprocessor.
|
|
||||||
|
|
||||||
### Lots of small files
|
|
||||||
|
|
||||||
Highly modular, highly functional, lots of single-function files. Tends to help with tree-shaking and
|
|
||||||
code-splitting, as well as avoiding circular dependencies.
|
|
||||||
|
|
||||||
### Inferno is loaded dynamically
|
|
||||||
|
|
||||||
This is a Svelte project, but `emoji-mart` is used for the emoji picker, and it's written in React. So we
|
|
||||||
lazy-load the React-compatible Inferno library when we load `emoji-mart`.
|
|
||||||
|
|
||||||
### Some third-party code is bundled
|
|
||||||
|
|
||||||
For various reasons, `a11y-dialog`, `autosize`, and `timeago` are forked and bundled into the source code.
|
|
||||||
This was either because something needed to be tweaked or fixed, or I was trimming unused code and didn't
|
|
||||||
see much value in contributing it back, because it was too Pinafore-specific.
|
|
||||||
|
|
||||||
### Every Sapper page is "duplicated"
|
|
||||||
|
|
||||||
To get a nice animation on the nav bar when you switch columns, every page is lazy-loaded as `LazyPage.html`.
|
|
||||||
This "lazy page" is merely delayed a few frames to let the animation run. Therefore there is a duplication
|
|
||||||
between `src/routes` and `src/routes/_pages`. The "lazy page" is in the former, and the actual page is in the
|
|
||||||
latter. One imports the other.
|
|
||||||
|
|
||||||
### There are multiple stores
|
|
||||||
|
|
||||||
Originally I conceived of separating out the virtual list into a separate npm package, so I gave it its
|
|
||||||
own Svelte store (`virtualListStore.js`). This never happened, but it still has its own store. This is useful
|
|
||||||
anyway, because each store has its state maintained in an LRU cache that allows us to keep the scroll position
|
|
||||||
in the virtual list e.g. when the user hits the back button.
|
|
||||||
|
|
||||||
Also, the main `store.js` store is explicitly
|
|
||||||
loaded by every component that uses it. So there's no `store` inheritance; every component just declares
|
|
||||||
whatever store it uses. The main `store.js` is the primary one.
|
|
||||||
|
|
||||||
### There is a global event bus
|
|
||||||
|
|
||||||
It's in `eventBus.js`. This is useful for some stuff that is hard to do with standard Svelte or DOM events.
|
|
12
Dockerfile
|
@ -7,17 +7,17 @@ ADD . /app
|
||||||
|
|
||||||
# Install updates and NodeJS+Dependencies
|
# Install updates and NodeJS+Dependencies
|
||||||
RUN apk update && apk upgrade
|
RUN apk update && apk upgrade
|
||||||
RUN apk add nodejs npm git python build-base clang
|
RUN apk add nodejs git python build-base clang
|
||||||
|
|
||||||
# Install yarn
|
# Upgrading NPM
|
||||||
RUN npm i yarn -g
|
RUN npm i npm@latest -g
|
||||||
|
|
||||||
# Install Pinafore
|
# Install Pinafore
|
||||||
RUN yarn --pure-lockfile
|
RUN npm install
|
||||||
RUN yarn build
|
RUN npm run build
|
||||||
|
|
||||||
# Expose port 4002
|
# Expose port 4002
|
||||||
EXPOSE 4002
|
EXPOSE 4002
|
||||||
|
|
||||||
# Setting run-command
|
# Setting run-command
|
||||||
CMD PORT=4002 yarn start
|
CMD PORT=4002 npm start
|
61
README.md
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
An alternative web client for [Mastodon](https://joinmastodon.org), focused on speed and simplicity.
|
An alternative web client for [Mastodon](https://joinmastodon.org), focused on speed and simplicity.
|
||||||
|
|
||||||
Pinafore is available at [pinafore.social](https://pinafore.social). Beta releases are at [dev.pinafore.social](https://dev.pinafore.social).
|
Pinafore is available at [pinafore.social](https://pinafore.social). Bleeding-edge releases are at [dev.pinafore.social](https://dev.pinafore.social).
|
||||||
|
|
||||||
See the [user guide](https://github.com/nolanlawson/pinafore/blob/master/docs/User-Guide.md) for basic usage. See the [admin guide](https://github.com/nolanlawson/pinafore/blob/master/docs/Admin-Guide.md) if Pinafore cannot connect to your instance.
|
See the [user guide](https://github.com/nolanlawson/pinafore/blob/master/docs/User-Guide.md) for basic usage. See the [admin guide](https://github.com/nolanlawson/pinafore/blob/master/docs/Admin-Guide.md) to troubleshoot instance compatibility issues.
|
||||||
|
|
||||||
For updates and support, follow [@pinafore@mastodon.technology](https://mastodon.technology/@pinafore).
|
For updates and support, follow us at [@pinafore@mastodon.technology](https://mastodon.technology/@pinafore).
|
||||||
|
|
||||||
## Browser support
|
## Browser support
|
||||||
|
|
||||||
|
@ -24,68 +24,51 @@ Compatible versions of each (Opera, Brave, Samsung, etc.) should be fine.
|
||||||
### Goals
|
### Goals
|
||||||
|
|
||||||
- Support the most common use cases
|
- Support the most common use cases
|
||||||
- Small page weight
|
- Fast even on low-end phones
|
||||||
- Fast even on low-end devices
|
- Works offline in read-only mode
|
||||||
- Accessibility
|
|
||||||
- Offline support in read-only mode
|
|
||||||
- Progressive Web App features
|
- Progressive Web App features
|
||||||
- Multi-instance support
|
- Multi-instance support
|
||||||
- Support latest versions of Chrome, Edge, Firefox, and Safari
|
- Support latest versions of Chrome, Edge, Firefox, and Safari
|
||||||
|
- a11y (keyboard navigation, screen readers)
|
||||||
|
|
||||||
### Secondary / possible future goals
|
### Possible future goals
|
||||||
|
|
||||||
- Support for Pleroma or other non-Mastodon backends
|
- Works as an alternative frontend self-hosted by instances
|
||||||
- Serve as an alternative frontend tied to a particular instance
|
- Android/iOS apps (using Cordova or similar)
|
||||||
- Support for non-English languages (i18n)
|
- Support Pleroma/non-Mastodon backends
|
||||||
|
- i18n
|
||||||
- Offline search
|
- Offline search
|
||||||
|
- Full emoji keyboard
|
||||||
|
- Keyboard shortcuts
|
||||||
|
|
||||||
### Non-goals
|
### Non-goals
|
||||||
|
|
||||||
- Supporting old browsers, proxy browsers, or text-based browsers
|
- Supporting old browsers, proxy browsers, or text-based browsers
|
||||||
- React Native / NativeScript / hybrid-native version
|
- React Native / NativeScript / hybrid-native version
|
||||||
- Android/iOS apps (using Cordova or similar)
|
|
||||||
- Full functionality with JavaScript disabled
|
- Full functionality with JavaScript disabled
|
||||||
- Emoji support beyond the built-in system emoji
|
- Emoji support beyond the built-in system emoji
|
||||||
- Multi-column support
|
- Multi-column support
|
||||||
- Admin/moderation panel
|
- Admin/moderation panel
|
||||||
- Offline support in read-write mode (would require sophisticated sync logic)
|
- Works offline in read-write mode (would require sophisticated sync logic)
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Pinafore requires [Node.js](https://nodejs.org/en/) v8+ and [Yarn](https://yarnpkg.com).
|
|
||||||
|
|
||||||
To build Pinafore for production:
|
To build Pinafore for production:
|
||||||
|
|
||||||
yarn --pure-lockfile
|
npm install
|
||||||
yarn build
|
npm run build
|
||||||
PORT=4002 yarn start
|
PORT=4002 npm start
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
To build a Docker image for production:
|
To build a docker image for production:
|
||||||
|
|
||||||
docker build .
|
docker build .
|
||||||
docker run -d -p 4002:4002 [your-image]
|
docker run -d -p 4002:4002 [your-image]
|
||||||
|
|
||||||
Now Pinafore is running at `localhost:4002`.
|
Now Pinafore is running at `localhost:4002`.
|
||||||
|
|
||||||
### Updating
|
Pinafore requires [Node.js](https://nodejs.org/en/) v8+.
|
||||||
|
|
||||||
To keep your version of Pinafore up to date, you can use `git` to check out the latest tag:
|
|
||||||
|
|
||||||
git checkout $(git tag -l | sort -Vr | head -n 1)
|
|
||||||
|
|
||||||
### Exporting
|
|
||||||
|
|
||||||
You can export Pinafore as a static site. Run:
|
|
||||||
|
|
||||||
yarn run export
|
|
||||||
|
|
||||||
Static files will be written to `__sapper__/export`.
|
|
||||||
|
|
||||||
Note that this is not the recommended method, because
|
|
||||||
[CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) headers are not
|
|
||||||
currently supported for the exported version.
|
|
||||||
|
|
||||||
## Developing and testing
|
## Developing and testing
|
||||||
|
|
||||||
|
@ -95,9 +78,3 @@ how to run Pinafore in dev mode and run tests.
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
For a changelog, see the [GitHub releases](http://github.com/nolanlawson/pinafore/releases/).
|
For a changelog, see the [GitHub releases](http://github.com/nolanlawson/pinafore/releases/).
|
||||||
|
|
||||||
For a list of breaking changes, see [BREAKING_CHANGES.md](https://github.com/nolanlawson/pinafore/blob/master/BREAKING_CHANGES.md).
|
|
||||||
|
|
||||||
## What's with the name?
|
|
||||||
|
|
||||||
Pinafore is named after the [Gilbert and Sullivan play](https://en.wikipedia.org/wiki/Hms_pinafore). The [soundtrack](https://www.allmusic.com/album/gilbert-sullivan-hms-pinafore-1949-mw0001830483) is very good.
|
|
||||||
|
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 599 B After Width: | Height: | Size: 599 B |
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 514 B |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 660 B After Width: | Height: | Size: 660 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 682 B After Width: | Height: | Size: 682 B |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
@ -1,8 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -x
|
|
||||||
set -e
|
|
||||||
|
|
||||||
PGPASSWORD=pinafore pg_dump -U pinafore -w pinafore_development -h 127.0.0.1 > tests/fixtures/dump.sql
|
|
||||||
cd mastodon/public/system
|
|
||||||
tar -czf ../../../tests/fixtures/system.tgz .
|
|
|
@ -1,47 +1,32 @@
|
||||||
import crypto from 'crypto'
|
#!/usr/bin/env node
|
||||||
import fs from 'fs'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
import path from 'path'
|
|
||||||
import { rollup } from 'rollup'
|
|
||||||
import { terser } from 'rollup-plugin-terser'
|
|
||||||
import replace from 'rollup-plugin-replace'
|
|
||||||
import fromPairs from 'lodash-es/fromPairs'
|
|
||||||
import { themes } from '../src/routes/_static/themes'
|
|
||||||
|
|
||||||
const writeFile = promisify(fs.writeFile)
|
const crypto = require('crypto')
|
||||||
|
const fs = require('fs')
|
||||||
|
const pify = require('pify')
|
||||||
|
const readFile = pify(fs.readFile.bind(fs))
|
||||||
|
const writeFile = pify(fs.writeFile.bind(fs))
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
const themeColors = fromPairs(themes.map(_ => ([_.name, _.color])))
|
async function main () {
|
||||||
|
let headScriptFilepath = path.join(__dirname, '../inline-script.js')
|
||||||
|
let headScript = await readFile(headScriptFilepath, 'utf8')
|
||||||
|
headScript = `(function () {'use strict'; ${headScript}})()`
|
||||||
|
|
||||||
export async function buildInlineScript () {
|
let checksum = crypto.createHash('sha256').update(headScript).digest('base64')
|
||||||
let inlineScriptPath = path.join(__dirname, '../src/inline-script/inline-script.js')
|
|
||||||
|
|
||||||
let bundle = await rollup({
|
let checksumFilepath = path.join(__dirname, '../inline-script-checksum.json')
|
||||||
input: inlineScriptPath,
|
await writeFile(checksumFilepath, JSON.stringify({checksum}), 'utf8')
|
||||||
plugins: [
|
|
||||||
replace({
|
|
||||||
'process.browser': true,
|
|
||||||
'process.env.THEME_COLORS': JSON.stringify(themeColors)
|
|
||||||
}),
|
|
||||||
terser({
|
|
||||||
mangle: true,
|
|
||||||
compress: true
|
|
||||||
})
|
|
||||||
]
|
|
||||||
})
|
|
||||||
let { output } = await bundle.generate({
|
|
||||||
format: 'iife',
|
|
||||||
sourcemap: true
|
|
||||||
})
|
|
||||||
|
|
||||||
let { code, map } = output[0]
|
let html2xxFilepath = path.join(__dirname, '../templates/2xx.html')
|
||||||
|
let html2xxFile = await readFile(html2xxFilepath, 'utf8')
|
||||||
let fullCode = `${code}//# sourceMappingURL=/inline-script.js.map`
|
html2xxFile = html2xxFile.replace(
|
||||||
let checksum = crypto.createHash('sha256').update(fullCode).digest('base64')
|
/<!-- insert inline script here -->[\s\S]+<!-- end insert inline script here -->/,
|
||||||
|
'<!-- insert inline script here --><script>' + headScript + '</script><!-- end insert inline script here -->'
|
||||||
await writeFile(path.resolve(__dirname, '../src/inline-script/checksum.js'),
|
)
|
||||||
`module.exports = ${JSON.stringify(checksum)}`, 'utf8')
|
await writeFile(html2xxFilepath, html2xxFile, 'utf8')
|
||||||
await writeFile(path.resolve(__dirname, '../static/inline-script.js.map'),
|
|
||||||
map.toString(), 'utf8')
|
|
||||||
|
|
||||||
return '<script>' + fullCode + '</script>'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
|
@ -1,46 +1,73 @@
|
||||||
import sass from 'node-sass'
|
#!/usr/bin/env node
|
||||||
import path from 'path'
|
|
||||||
import fs from 'fs'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
import cssDedoupe from 'css-dedoupe'
|
|
||||||
import { TextDecoder } from 'text-encoding'
|
|
||||||
|
|
||||||
const writeFile = promisify(fs.writeFile)
|
const sass = require('node-sass')
|
||||||
const readdir = promisify(fs.readdir)
|
const chokidar = require('chokidar')
|
||||||
const render = promisify(sass.render.bind(sass))
|
const argv = require('yargs').argv
|
||||||
|
const path = require('path')
|
||||||
|
const debounce = require('lodash/debounce')
|
||||||
|
const fs = require('fs')
|
||||||
|
const pify = require('pify')
|
||||||
|
const writeFile = pify(fs.writeFile.bind(fs))
|
||||||
|
const readdir = pify(fs.readdir.bind(fs))
|
||||||
|
const readFile = pify(fs.readFile.bind(fs))
|
||||||
|
const render = pify(sass.render.bind(sass))
|
||||||
|
const now = require('performance-now')
|
||||||
|
|
||||||
const globalScss = path.join(__dirname, '../src/scss/global.scss')
|
const globalScss = path.join(__dirname, '../scss/global.scss')
|
||||||
const defaultThemeScss = path.join(__dirname, '../src/scss/themes/_default.scss')
|
const defaultThemeScss = path.join(__dirname, '../scss/themes/_default.scss')
|
||||||
const offlineThemeScss = path.join(__dirname, '../src/scss/themes/_offline.scss')
|
const offlineThemeScss = path.join(__dirname, '../scss/themes/_offline.scss')
|
||||||
const customScrollbarScss = path.join(__dirname, '../src/scss/custom-scrollbars.scss')
|
const html2xxFile = path.join(__dirname, '../templates/2xx.html')
|
||||||
const themesScssDir = path.join(__dirname, '../src/scss/themes')
|
const scssDir = path.join(__dirname, '../scss')
|
||||||
const assetsDir = path.join(__dirname, '../static')
|
const themesScssDir = path.join(__dirname, '../scss/themes')
|
||||||
|
const assetsDir = path.join(__dirname, '../assets')
|
||||||
|
|
||||||
async function renderCss (file) {
|
function doWatch () {
|
||||||
return (await render({ file, outputStyle: 'compressed' })).css
|
let start = now()
|
||||||
|
chokidar.watch(scssDir).on('change', debounce(() => {
|
||||||
|
console.log('Recompiling SCSS...')
|
||||||
|
Promise.all([
|
||||||
|
compileGlobalSass(),
|
||||||
|
compileThemesSass()
|
||||||
|
]).then(() => {
|
||||||
|
console.log('Recompiled SCSS in ' + (now() - start) + 'ms')
|
||||||
|
})
|
||||||
|
}, 500))
|
||||||
|
chokidar.watch()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function compileGlobalSass () {
|
async function compileGlobalSass () {
|
||||||
let mainStyle = (await Promise.all([defaultThemeScss, globalScss].map(renderCss))).join('')
|
let results = await Promise.all([
|
||||||
let offlineStyle = (await renderCss(offlineThemeScss))
|
render({file: defaultThemeScss, outputStyle: 'compressed'}),
|
||||||
let scrollbarStyle = (await renderCss(customScrollbarScss))
|
render({file: globalScss, outputStyle: 'compressed'}),
|
||||||
|
render({file: offlineThemeScss, outputStyle: 'compressed'})
|
||||||
|
])
|
||||||
|
|
||||||
return `<style>\n${mainStyle}</style>\n` +
|
let css = results.map(_ => _.css).join('')
|
||||||
`<style media="only x" id="theOfflineStyle">\n${offlineStyle}</style>\n` +
|
|
||||||
`<style media="all" id="theScrollbarStyle">\n${scrollbarStyle}</style>\n`
|
let html = await readFile(html2xxFile, 'utf8')
|
||||||
|
html = html.replace(/<style>[\s\S]+?<\/style>/,
|
||||||
|
`<style>\n/* auto-generated w/ build-sass.js */\n${css}\n</style>`)
|
||||||
|
|
||||||
|
await writeFile(html2xxFile, html, 'utf8')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function compileThemesSass () {
|
async function compileThemesSass () {
|
||||||
let files = (await readdir(themesScssDir)).filter(file => !path.basename(file).startsWith('_'))
|
let files = (await readdir(themesScssDir)).filter(file => !path.basename(file).startsWith('_'))
|
||||||
await Promise.all(files.map(async file => {
|
await Promise.all(files.map(async file => {
|
||||||
let css = await renderCss(path.join(themesScssDir, file))
|
let res = await render({file: path.join(themesScssDir, file), outputStyle: 'compressed'})
|
||||||
css = cssDedoupe(new TextDecoder('utf-8').decode(css)) // remove duplicate custom properties
|
|
||||||
let outputFilename = 'theme-' + path.basename(file).replace(/\.scss$/, '.css')
|
let outputFilename = 'theme-' + path.basename(file).replace(/\.scss$/, '.css')
|
||||||
await writeFile(path.join(assetsDir, outputFilename), css, 'utf8')
|
await writeFile(path.join(assetsDir, outputFilename), res.css, 'utf8')
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildSass () {
|
async function main () {
|
||||||
let [ result ] = await Promise.all([compileGlobalSass(), compileThemesSass()])
|
await Promise.all([compileGlobalSass(), compileThemesSass()])
|
||||||
return result
|
if (argv.watch) {
|
||||||
|
doWatch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Promise.resolve().then(main).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import svgs from './svgs'
|
#!/usr/bin/env node
|
||||||
import path from 'path'
|
|
||||||
import fs from 'fs'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
import SVGO from 'svgo'
|
|
||||||
import $ from 'cheerio'
|
|
||||||
|
|
||||||
|
const svgs = require('./svgs')
|
||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
const pify = require('pify')
|
||||||
|
const SVGO = require('svgo')
|
||||||
const svgo = new SVGO()
|
const svgo = new SVGO()
|
||||||
const readFile = promisify(fs.readFile)
|
const $ = require('cheerio')
|
||||||
|
|
||||||
export async function buildSvg () {
|
const readFile = pify(fs.readFile.bind(fs))
|
||||||
|
const writeFile = pify(fs.writeFile.bind(fs))
|
||||||
|
|
||||||
|
async function main () {
|
||||||
let result = (await Promise.all(svgs.map(async svg => {
|
let result = (await Promise.all(svgs.map(async svg => {
|
||||||
let filepath = path.join(__dirname, '../', svg.src)
|
let filepath = path.join(__dirname, '../', svg.src)
|
||||||
let content = await readFile(filepath, 'utf8')
|
let content = await readFile(filepath, 'utf8')
|
||||||
|
@ -18,9 +21,23 @@ export async function buildSvg () {
|
||||||
let $symbol = $('<symbol></symbol>')
|
let $symbol = $('<symbol></symbol>')
|
||||||
.attr('id', svg.id)
|
.attr('id', svg.id)
|
||||||
.attr('viewBox', `0 0 ${optimized.info.width} ${optimized.info.height}`)
|
.attr('viewBox', `0 0 ${optimized.info.width} ${optimized.info.height}`)
|
||||||
|
.append($('<title></title>').text(svg.title))
|
||||||
.append($path)
|
.append($path)
|
||||||
return $.xml($symbol)
|
return $.xml($symbol)
|
||||||
}))).join('\n')
|
}))).join('\n')
|
||||||
|
|
||||||
return `<svg xmlns="http://www.w3.org/2000/svg" style="display:none;">\n${result}\n</svg>`
|
result = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none;">\n${result}\n</svg>`
|
||||||
|
|
||||||
|
let html2xxFilepath = path.join(__dirname, '../templates/2xx.html')
|
||||||
|
let html2xxFile = await readFile(html2xxFilepath, 'utf8')
|
||||||
|
html2xxFile = html2xxFile.replace(
|
||||||
|
/<!-- insert svg here -->[\s\S]+<!-- end insert svg here -->/,
|
||||||
|
'<!-- insert svg here -->' + result + '<!-- end insert svg here -->'
|
||||||
|
)
|
||||||
|
await writeFile(html2xxFilepath, html2xxFile, 'utf8')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
import chokidar from 'chokidar'
|
|
||||||
import fs from 'fs'
|
|
||||||
import path from 'path'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
import { buildSass } from './build-sass'
|
|
||||||
import { buildInlineScript } from './build-inline-script'
|
|
||||||
import { buildSvg } from './build-svg'
|
|
||||||
import now from 'performance-now'
|
|
||||||
import debounce from 'lodash-es/debounce'
|
|
||||||
|
|
||||||
const writeFile = promisify(fs.writeFile)
|
|
||||||
|
|
||||||
const DEBOUNCE = 500
|
|
||||||
|
|
||||||
const builders = [
|
|
||||||
{
|
|
||||||
watch: 'src/scss',
|
|
||||||
comment: '<!-- inline CSS -->',
|
|
||||||
rebuild: buildSass
|
|
||||||
},
|
|
||||||
{
|
|
||||||
watch: 'src/inline-script/inline-script.js',
|
|
||||||
comment: '<!-- inline JS -->',
|
|
||||||
rebuild: buildInlineScript
|
|
||||||
},
|
|
||||||
{
|
|
||||||
watch: 'bin/svgs.js',
|
|
||||||
comment: '<!-- inline SVG -->',
|
|
||||||
rebuild: buildSvg
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// array of strings and builder functions, we build this on-the-fly
|
|
||||||
const partials = buildPartials()
|
|
||||||
|
|
||||||
function buildPartials () {
|
|
||||||
let rawTemplate = fs.readFileSync(path.resolve(__dirname, '../src/build/template.html'), 'utf8')
|
|
||||||
|
|
||||||
let partials = [rawTemplate]
|
|
||||||
|
|
||||||
builders.forEach(builder => {
|
|
||||||
for (let i = 0; i < partials.length; i++) {
|
|
||||||
let partial = partials[i]
|
|
||||||
if (typeof partial !== 'string') {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
let idx = partial.indexOf(builder.comment)
|
|
||||||
if (idx !== -1) {
|
|
||||||
partials.splice(
|
|
||||||
i,
|
|
||||||
1,
|
|
||||||
partial.substring(0, idx),
|
|
||||||
builder,
|
|
||||||
partial.substring(idx + builder.comment.length)
|
|
||||||
)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return partials
|
|
||||||
}
|
|
||||||
|
|
||||||
function doWatch () {
|
|
||||||
// rebuild each of the partials on-the-fly if something changes
|
|
||||||
partials.forEach(partial => {
|
|
||||||
if (typeof partial === 'string') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
chokidar.watch(partial.watch).on('change', debounce(path => {
|
|
||||||
console.log(`Detected change in ${path}...`)
|
|
||||||
delete partial.result
|
|
||||||
buildAll()
|
|
||||||
}), DEBOUNCE)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildAll () {
|
|
||||||
let start = now()
|
|
||||||
let html = (await Promise.all(partials.map(async partial => {
|
|
||||||
if (typeof partial === 'string') {
|
|
||||||
return partial
|
|
||||||
}
|
|
||||||
if (!partial.result) {
|
|
||||||
partial.result = partial.comment + '\n' + (await partial.rebuild())
|
|
||||||
}
|
|
||||||
return partial.result
|
|
||||||
}))).join('')
|
|
||||||
|
|
||||||
await writeFile(path.resolve(__dirname, '../src/template.html'), html, 'utf8')
|
|
||||||
let end = now()
|
|
||||||
console.log(`Built template.html in ${(end - start).toFixed(2)}ms`)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main () {
|
|
||||||
if (process.argv.includes('--watch')) {
|
|
||||||
doWatch()
|
|
||||||
} else {
|
|
||||||
await buildAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(err => {
|
|
||||||
console.error(err)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
|
@ -1,33 +0,0 @@
|
||||||
import path from 'path'
|
|
||||||
import fs from 'fs'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
import CleanCSS from 'clean-css'
|
|
||||||
|
|
||||||
const writeFile = promisify(fs.writeFile)
|
|
||||||
const readFile = promisify(fs.readFile)
|
|
||||||
const copyFile = promisify(fs.copyFile)
|
|
||||||
|
|
||||||
async function compileThirdPartyCss () {
|
|
||||||
let css = await readFile(path.resolve(__dirname, '../node_modules/emoji-mart/css/emoji-mart.css'), 'utf8')
|
|
||||||
css = `/* compiled from emoji-mart.css */` + new CleanCSS().minify(css).styles
|
|
||||||
await writeFile(path.resolve(__dirname, '../static/emoji-mart.css'), css, 'utf8')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function compileThirdPartyJson () {
|
|
||||||
await copyFile(
|
|
||||||
path.resolve(__dirname, '../node_modules/emoji-mart/data/all.json'),
|
|
||||||
path.resolve(__dirname, '../static/emoji-mart-all.json')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main () {
|
|
||||||
await Promise.all([
|
|
||||||
compileThirdPartyCss(),
|
|
||||||
compileThirdPartyJson()
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(err => {
|
|
||||||
console.error(err)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
|
@ -1,8 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
set -x
|
|
||||||
|
|
||||||
if [ "$TRAVIS_BRANCH" = master -a "$TRAVIS_PULL_REQUEST" = false ]; then
|
|
||||||
yarn run deploy-dev
|
|
||||||
fi
|
|
|
@ -1,38 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
set -x
|
|
||||||
|
|
||||||
PATH="$PATH:./node_modules/.bin"
|
|
||||||
|
|
||||||
# set up robots.txt
|
|
||||||
if [[ "$DEPLOY_TYPE" == "dev" ]]; then
|
|
||||||
printf 'User-agent: *\nDisallow: /' > static/robots.txt
|
|
||||||
else
|
|
||||||
rm -f static/robots.txt
|
|
||||||
fi
|
|
||||||
|
|
||||||
# if in travis, use the $NOW_TOKEN
|
|
||||||
NOW_COMMAND="now --team nolanlawson"
|
|
||||||
if [[ ! -z "$NOW_TOKEN" ]]; then
|
|
||||||
NOW_COMMAND="$NOW_COMMAND --token $NOW_TOKEN"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# launch
|
|
||||||
URL=$($NOW_COMMAND -e SAPPER_TIMESTAMP=$(date +%s%3N))
|
|
||||||
|
|
||||||
# fixes issues with now being unavailable immediately
|
|
||||||
sleep 60
|
|
||||||
|
|
||||||
# choose the right alias
|
|
||||||
NOW_ALIAS="dev.pinafore.social"
|
|
||||||
|
|
||||||
if [[ "$DEPLOY_TYPE" == "prod" ]]; then
|
|
||||||
NOW_ALIAS="pinafore.social"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# alias
|
|
||||||
$NOW_COMMAND alias "$URL" "$NOW_ALIAS"
|
|
||||||
|
|
||||||
# cleanup
|
|
||||||
$NOW_COMMAND rm pinafore --safe --yes
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// Change all the Svelte CSS to just use globals everywhere,
|
||||||
|
// to reduce CSS size and complexity.
|
||||||
|
|
||||||
|
const argv = require('yargs').argv
|
||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
const pify = require('pify')
|
||||||
|
const writeFile = pify(fs.writeFile.bind(fs))
|
||||||
|
const readFile = pify(fs.readFile.bind(fs))
|
||||||
|
const glob = pify(require('glob'))
|
||||||
|
const rimraf = pify(require('rimraf'))
|
||||||
|
|
||||||
|
const selectorRegex = /\n[ \t\n]*([0-9\w\- \t\n.:#,]+?)[ \t\n]*{/g
|
||||||
|
const styleRegex = /<style>[\s\S]+?<\/style>/
|
||||||
|
|
||||||
|
async function main () {
|
||||||
|
if (argv.reverse) { // reverse the operation we just did
|
||||||
|
let tmpComponents = await glob('./routes/**/.tmp-*.html')
|
||||||
|
for (let filename of tmpComponents) {
|
||||||
|
let text = await readFile(filename, 'utf8')
|
||||||
|
await rimraf(filename)
|
||||||
|
let originalFilename = path.join(path.dirname(filename), path.basename(filename).substring(5))
|
||||||
|
await writeFile(originalFilename, text, 'utf8')
|
||||||
|
}
|
||||||
|
} else { // read all files, copy to tmp files, rewrite files to include global CSS everywhere
|
||||||
|
let components = await glob('./routes/**/*.html')
|
||||||
|
for (let filename of components) {
|
||||||
|
let text = await readFile(filename, 'utf8')
|
||||||
|
let newText = text.replace(styleRegex, style => {
|
||||||
|
return style.replace(selectorRegex, selectorMatch => {
|
||||||
|
return selectorMatch.replace(/\S[^{]+/, selector => `:global(${selector})`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
let newFilename = path.join(path.dirname(filename), '.tmp-' + path.basename(filename))
|
||||||
|
|
||||||
|
await writeFile(newFilename, text, 'utf8')
|
||||||
|
await writeFile(filename, newText, 'utf8')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.resolve().then(main).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
|
@ -1,28 +0,0 @@
|
||||||
console.log(`
|
|
||||||
|
|
||||||
,((*
|
|
||||||
,((* (,
|
|
||||||
,((* (((*
|
|
||||||
,((* (((((.
|
|
||||||
* ,((* ((((((*
|
|
||||||
.(/ ,((* (((((((/
|
|
||||||
.((/ ,((* ((((((((/
|
|
||||||
,(((/ ,((* (((((((((*
|
|
||||||
.(((((/ ,((* ((((((((((
|
|
||||||
,((*
|
|
||||||
//////////((((/////////////
|
|
||||||
/((((((((((((((((((((((((((
|
|
||||||
/((((((((((((((((((((((((,
|
|
||||||
*(((((((((((((((((((((/.
|
|
||||||
./((((((((((((((((.
|
|
||||||
|
|
||||||
|
|
||||||
P I N A F O R E
|
|
||||||
|
|
||||||
|
|
||||||
Export successful! Static files are in:
|
|
||||||
|
|
||||||
__sapper__/export/
|
|
||||||
|
|
||||||
Enjoy Pinafore!
|
|
||||||
`)
|
|
|
@ -1,38 +1,64 @@
|
||||||
import { actions } from './mastodon-data'
|
import { actions } from './mastodon-data'
|
||||||
import { users } from '../tests/users'
|
import { users } from '../tests/users'
|
||||||
import { postStatus } from '../src/routes/_api/statuses'
|
import { postStatus } from '../routes/_api/statuses'
|
||||||
import { followAccount } from '../src/routes/_api/follow'
|
import { followAccount } from '../routes/_api/follow'
|
||||||
import { favoriteStatus } from '../src/routes/_api/favorite'
|
import { favoriteStatus } from '../routes/_api/favorite'
|
||||||
import { reblogStatus } from '../src/routes/_api/reblog'
|
import { reblogStatus } from '../routes/_api/reblog'
|
||||||
import fetch from 'node-fetch'
|
import fetch from 'node-fetch'
|
||||||
import FileApi from 'file-api'
|
import FileApi from 'file-api'
|
||||||
import { pinStatus } from '../src/routes/_api/pin'
|
import path from 'path'
|
||||||
import { submitMedia } from '../tests/submitMedia'
|
import fs from 'fs'
|
||||||
|
import FormData from 'form-data'
|
||||||
|
import { auth } from '../routes/_api/utils'
|
||||||
|
import { pinStatus } from '../routes/_api/pin'
|
||||||
|
|
||||||
global.File = FileApi.File
|
global.File = FileApi.File
|
||||||
global.FormData = FileApi.FormData
|
global.FormData = FileApi.FormData
|
||||||
global.fetch = fetch
|
global.fetch = fetch
|
||||||
|
|
||||||
|
async function submitMedia (accessToken, filename, alt) {
|
||||||
|
let form = new FormData()
|
||||||
|
form.append('file', fs.createReadStream(path.join(__dirname, '../tests/images/' + filename)))
|
||||||
|
form.append('description', alt)
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
form.submit({
|
||||||
|
host: 'localhost',
|
||||||
|
port: 3000,
|
||||||
|
path: '/api/v1/media',
|
||||||
|
headers: auth(accessToken)
|
||||||
|
}, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err)
|
||||||
|
}
|
||||||
|
let data = ''
|
||||||
|
|
||||||
|
res.on('data', chunk => {
|
||||||
|
data += chunk
|
||||||
|
})
|
||||||
|
|
||||||
|
res.on('end', () => resolve(JSON.parse(data)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function restoreMastodonData () {
|
export async function restoreMastodonData () {
|
||||||
console.log('Restoring mastodon data...')
|
console.log('Restoring mastodon data...')
|
||||||
let internalIdsToIds = {}
|
let internalIdsToIds = {}
|
||||||
for (let action of actions) {
|
for (let action of actions) {
|
||||||
if (!action.post) {
|
await new Promise(resolve => setTimeout(resolve, 1000)) // delay so that notifications have proper order
|
||||||
// If the action is a boost, favorite, etc., then it needs to
|
|
||||||
// be delayed, otherwise it may appear in an unpredictable order and break the tests.
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
||||||
}
|
|
||||||
console.log(JSON.stringify(action))
|
console.log(JSON.stringify(action))
|
||||||
let accessToken = users[action.user].accessToken
|
let accessToken = users[action.user].accessToken
|
||||||
|
|
||||||
if (action.post) {
|
if (action.post) {
|
||||||
let { text, media, sensitive, spoiler, privacy, inReplyTo, internalId } = action.post
|
let { text, media, sensitive, spoiler, privacy, inReplyTo, internalId } = action.post
|
||||||
|
if (typeof inReplyTo !== 'undefined') {
|
||||||
|
inReplyTo = internalIdsToIds[inReplyTo]
|
||||||
|
}
|
||||||
let mediaIds = media && await Promise.all(media.map(async mediaItem => {
|
let mediaIds = media && await Promise.all(media.map(async mediaItem => {
|
||||||
let mediaResponse = await submitMedia(accessToken, mediaItem, 'kitten')
|
let mediaResponse = await submitMedia(accessToken, mediaItem, 'kitten')
|
||||||
return mediaResponse.id
|
return mediaResponse.id
|
||||||
}))
|
}))
|
||||||
let inReplyToId = inReplyTo && internalIdsToIds[inReplyTo]
|
let status = await postStatus('localhost:3000', accessToken, text, inReplyTo, mediaIds,
|
||||||
let status = await postStatus('localhost:3000', accessToken, text, inReplyToId, mediaIds,
|
|
||||||
sensitive, spoiler, privacy || 'public')
|
sensitive, spoiler, privacy || 'public')
|
||||||
if (typeof internalId !== 'undefined') {
|
if (typeof internalId !== 'undefined') {
|
||||||
internalIdsToIds[internalId] = status.id
|
internalIdsToIds[internalId] = status.id
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { restoreMastodonData } from './restore-mastodon-data'
|
import { restoreMastodonData } from './restore-mastodon-data'
|
||||||
import { promisify } from 'util'
|
import pify from 'pify'
|
||||||
import childProcessPromise from 'child-process-promise'
|
import childProcessPromise from 'child-process-promise'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
@ -8,13 +8,13 @@ import mkdirpCB from 'mkdirp'
|
||||||
|
|
||||||
const exec = childProcessPromise.exec
|
const exec = childProcessPromise.exec
|
||||||
const spawn = childProcessPromise.spawn
|
const spawn = childProcessPromise.spawn
|
||||||
const mkdirp = promisify(mkdirpCB)
|
const mkdirp = pify(mkdirpCB)
|
||||||
const stat = promisify(fs.stat)
|
const stat = pify(fs.stat.bind(fs))
|
||||||
const writeFile = promisify(fs.writeFile)
|
const writeFile = pify(fs.writeFile.bind(fs))
|
||||||
const dir = __dirname
|
const dir = __dirname
|
||||||
|
|
||||||
const GIT_URL = 'https://github.com/tootsuite/mastodon.git'
|
const GIT_URL = 'https://github.com/tootsuite/mastodon.git'
|
||||||
const GIT_TAG = 'v2.7.0'
|
const GIT_TAG = 'v2.4.0'
|
||||||
|
|
||||||
const DB_NAME = 'pinafore_development'
|
const DB_NAME = 'pinafore_development'
|
||||||
const DB_USER = 'pinafore'
|
const DB_USER = 'pinafore'
|
||||||
|
@ -43,7 +43,6 @@ async function cloneMastodon () {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Cloning mastodon...')
|
console.log('Cloning mastodon...')
|
||||||
await exec(`git clone --single-branch --branch master ${GIT_URL} "${mastodonDir}"`)
|
await exec(`git clone --single-branch --branch master ${GIT_URL} "${mastodonDir}"`)
|
||||||
await exec(`git fetch origin --tags`, { cwd: mastodonDir }) // may already be cloned, e.g. in CI
|
|
||||||
await exec(`git checkout ${GIT_TAG}`, { cwd: mastodonDir })
|
await exec(`git checkout ${GIT_TAG}`, { cwd: mastodonDir })
|
||||||
await writeFile(path.join(dir, '../mastodon/.env'), envFile, 'utf8')
|
await writeFile(path.join(dir, '../mastodon/.env'), envFile, 'utf8')
|
||||||
}
|
}
|
||||||
|
@ -57,24 +56,24 @@ async function setupMastodonDatabase () {
|
||||||
try {
|
try {
|
||||||
await exec(`dropdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
await exec(`dropdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
||||||
cwd: mastodonDir,
|
cwd: mastodonDir,
|
||||||
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
|
env: Object.assign({PGPASSWORD: DB_PASS}, process.env)
|
||||||
})
|
})
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
await exec(`createdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
await exec(`createdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
||||||
cwd: mastodonDir,
|
cwd: mastodonDir,
|
||||||
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
|
env: Object.assign({PGPASSWORD: DB_PASS}, process.env)
|
||||||
})
|
})
|
||||||
|
|
||||||
let dumpFile = path.join(dir, '../tests/fixtures/dump.sql')
|
let dumpFile = path.join(dir, '../fixtures/dump.sql')
|
||||||
await exec(`psql -h 127.0.0.1 -U ${DB_USER} -w -d ${DB_NAME} -f "${dumpFile}"`, {
|
await exec(`psql -h 127.0.0.1 -U ${DB_USER} -w -d ${DB_NAME} -f "${dumpFile}"`, {
|
||||||
cwd: mastodonDir,
|
cwd: mastodonDir,
|
||||||
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
|
env: Object.assign({PGPASSWORD: DB_PASS}, process.env)
|
||||||
})
|
})
|
||||||
|
|
||||||
let tgzFile = path.join(dir, '../tests/fixtures/system.tgz')
|
let tgzFile = path.join(dir, '../fixtures/system.tgz')
|
||||||
let systemDir = path.join(mastodonDir, 'public/system')
|
let systemDir = path.join(mastodonDir, 'public/system')
|
||||||
await mkdirp(systemDir)
|
await mkdirp(systemDir)
|
||||||
await exec(`tar -xzf "${tgzFile}"`, { cwd: systemDir })
|
await exec(`tar -xzf "${tgzFile}"`, {cwd: systemDir})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runMastodon () {
|
async function runMastodon () {
|
||||||
|
@ -96,20 +95,12 @@ async function runMastodon () {
|
||||||
'yarn --pure-lockfile'
|
'yarn --pure-lockfile'
|
||||||
]
|
]
|
||||||
|
|
||||||
const installedFile = path.join(mastodonDir, 'installed.txt')
|
for (let cmd of cmds) {
|
||||||
try {
|
console.log(cmd)
|
||||||
await stat(installedFile)
|
await exec(cmd, {cwd, env})
|
||||||
console.log('Already installed Mastodon')
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Installing Mastodon...')
|
|
||||||
for (let cmd of cmds) {
|
|
||||||
console.log(cmd)
|
|
||||||
await exec(cmd, { cwd, env })
|
|
||||||
}
|
|
||||||
await writeFile(installedFile, '', 'utf8')
|
|
||||||
}
|
}
|
||||||
const promise = spawn('foreman', ['start'], { cwd, env })
|
const promise = spawn('foreman', ['start'], {cwd, env})
|
||||||
const log = fs.createWriteStream('mastodon.log', { flags: 'a' })
|
const log = fs.createWriteStream('mastodon.log', {flags: 'a'})
|
||||||
childProc = promise.childProcess
|
childProc = promise.childProcess
|
||||||
childProc.stdout.pipe(log)
|
childProc.stdout.pipe(log)
|
||||||
childProc.stderr.pipe(log)
|
childProc.stderr.pipe(log)
|
||||||
|
|
|
@ -2,39 +2,20 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if [[ "$COMMAND" = deploy-all-travis || "$COMMAND" = test-unit ]]; then
|
if [[ "$COMMAND" = deploy-dev-travis ]]; then
|
||||||
exit 0 # no need to setup mastodon in this case
|
exit 0 # no need to setup mastodon in this case
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# install ruby
|
|
||||||
source "$HOME/.rvm/scripts/rvm"
|
source "$HOME/.rvm/scripts/rvm"
|
||||||
rvm install 2.6.0
|
rvm install 2.5.1
|
||||||
rvm use 2.6.0
|
rvm use 2.5.1
|
||||||
|
|
||||||
# fix for redis IPv6 issue
|
sudo -E add-apt-repository -y ppa:mc3man/trusty-media
|
||||||
# https://travis-ci.community/t/trusty-environment-redis-server-not-starting-with-redis-tools-installed/650/2
|
sudo -E apt-get update
|
||||||
sudo sed -e 's/^bind.*/bind 127.0.0.1/' /etc/redis/redis.conf > redis.conf
|
sudo -E apt-get install -y ffmpeg
|
||||||
sudo mv redis.conf /etc/redis
|
|
||||||
sudo service redis-server start
|
|
||||||
echo PING | nc localhost 6379 # check redis running
|
|
||||||
|
|
||||||
# install ffmpeg because it's not in Trusty
|
|
||||||
if [ ! -f /home/travis/ffmpeg-static/ffmpeg ]; then
|
|
||||||
rm -fr /home/travis/ffmpeg-static
|
|
||||||
mkdir -p /home/travis/ffmpeg-static
|
|
||||||
curl -sL \
|
|
||||||
-A 'https://github.com/nolanlawson/pinafore' \
|
|
||||||
-o ffmpeg.tar.xz \
|
|
||||||
'https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-64bit-static.tar.xz'
|
|
||||||
tar -x -C /home/travis/ffmpeg-static --strip-components 1 -f ffmpeg.tar.xz --wildcards '*/ffmpeg' --wildcards '*/ffprobe'
|
|
||||||
fi
|
|
||||||
sudo ln -s /home/travis/ffmpeg-static/ffmpeg /usr/local/bin/ffmpeg
|
|
||||||
sudo ln -s /home/travis/ffmpeg-static/ffprobe /usr/local/bin/ffprobe
|
|
||||||
|
|
||||||
# check versions
|
|
||||||
ruby --version
|
ruby --version
|
||||||
node --version
|
node --version
|
||||||
yarn --version
|
npm --version
|
||||||
postgres --version
|
postgres --version
|
||||||
redis-server --version
|
redis-server --version
|
||||||
ffmpeg -version
|
ffmpeg -version
|
||||||
|
|
83
bin/svgs.js
|
@ -1,47 +1,40 @@
|
||||||
module.exports = [
|
module.exports = [
|
||||||
{ id: 'pinafore-logo', src: 'src/static/sailboat.svg' },
|
{id: 'pinafore-logo', src: 'original-assets/sailboat.svg', title: 'Home'},
|
||||||
{ id: 'fa-bell', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell.svg' },
|
{id: 'fa-bell', src: 'node_modules/font-awesome-svg-png/white/svg/bell.svg', title: 'Notifications'},
|
||||||
{ id: 'fa-users', src: 'src/thirdparty/font-awesome-svg-png/white/svg/users.svg' },
|
{id: 'fa-users', src: 'node_modules/font-awesome-svg-png/white/svg/users.svg', title: 'Local'},
|
||||||
{ id: 'fa-globe', src: 'src/thirdparty/font-awesome-svg-png/white/svg/globe.svg' },
|
{id: 'fa-globe', src: 'node_modules/font-awesome-svg-png/white/svg/globe.svg', title: 'Federated'},
|
||||||
{ id: 'fa-gear', src: 'src/thirdparty/font-awesome-svg-png/white/svg/gear.svg' },
|
{id: 'fa-gear', src: 'node_modules/font-awesome-svg-png/white/svg/gear.svg', title: 'Settings'},
|
||||||
{ id: 'fa-reply', src: 'src/thirdparty/font-awesome-svg-png/white/svg/reply.svg' },
|
{id: 'fa-reply', src: 'node_modules/font-awesome-svg-png/white/svg/reply.svg', title: 'Reply'},
|
||||||
{ id: 'fa-reply-all', src: 'src/thirdparty/font-awesome-svg-png/white/svg/reply-all.svg' },
|
{id: 'fa-reply-all', src: 'node_modules/font-awesome-svg-png/white/svg/reply-all.svg', title: 'Reply to thread'},
|
||||||
{ id: 'fa-retweet', src: 'src/thirdparty/font-awesome-svg-png/white/svg/retweet.svg' },
|
{id: 'fa-retweet', src: 'node_modules/font-awesome-svg-png/white/svg/retweet.svg', title: 'Boost'},
|
||||||
{ id: 'fa-star', src: 'src/thirdparty/font-awesome-svg-png/white/svg/star.svg' },
|
{id: 'fa-star', src: 'node_modules/font-awesome-svg-png/white/svg/star.svg', title: 'Favorite'},
|
||||||
{ id: 'fa-star-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/star-o.svg' },
|
{id: 'fa-ellipsis-h', src: 'node_modules/font-awesome-svg-png/white/svg/ellipsis-h.svg', title: 'More'},
|
||||||
{ id: 'fa-ellipsis-h', src: 'src/thirdparty/font-awesome-svg-png/white/svg/ellipsis-h.svg' },
|
{id: 'fa-spinner', src: 'node_modules/font-awesome-svg-png/white/svg/spinner.svg', title: 'Spinner'},
|
||||||
{ id: 'fa-spinner', src: 'src/thirdparty/font-awesome-svg-png/white/svg/spinner.svg' },
|
{id: 'fa-user', src: 'node_modules/font-awesome-svg-png/white/svg/user.svg', title: 'Empty user profile'},
|
||||||
{ id: 'fa-user', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user.svg' },
|
{id: 'fa-play-circle', src: 'node_modules/font-awesome-svg-png/white/svg/play-circle.svg', title: 'Play'},
|
||||||
{ id: 'fa-play-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/play-circle.svg' },
|
{id: 'fa-eye', src: 'node_modules/font-awesome-svg-png/white/svg/eye.svg', title: 'Show Sensitive Content'},
|
||||||
{ id: 'fa-eye', src: 'src/thirdparty/font-awesome-svg-png/white/svg/eye.svg' },
|
{id: 'fa-eye-slash', src: 'node_modules/font-awesome-svg-png/white/svg/eye-slash.svg', title: 'Hide Sensitive Content'},
|
||||||
{ id: 'fa-eye-slash', src: 'src/thirdparty/font-awesome-svg-png/white/svg/eye-slash.svg' },
|
{id: 'fa-lock', src: 'node_modules/font-awesome-svg-png/white/svg/lock.svg', title: 'Locked'},
|
||||||
{ id: 'fa-lock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/lock.svg' },
|
{id: 'fa-unlock', src: 'node_modules/font-awesome-svg-png/white/svg/unlock.svg', title: 'Unlocked'},
|
||||||
{ id: 'fa-unlock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/unlock.svg' },
|
{id: 'fa-envelope', src: 'node_modules/font-awesome-svg-png/white/svg/envelope.svg', title: 'Sealed Envelope'},
|
||||||
{ id: 'fa-envelope', src: 'src/thirdparty/font-awesome-svg-png/white/svg/envelope.svg' },
|
{id: 'fa-user-times', src: 'node_modules/font-awesome-svg-png/white/svg/user-times.svg', title: 'Stop Following'},
|
||||||
{ id: 'fa-user-times', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user-times.svg' },
|
{id: 'fa-user-plus', src: 'node_modules/font-awesome-svg-png/white/svg/user-plus.svg', title: 'Follow'},
|
||||||
{ id: 'fa-user-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user-plus.svg' },
|
{id: 'fa-external-link', src: 'node_modules/font-awesome-svg-png/white/svg/external-link.svg', title: 'External Link'},
|
||||||
{ id: 'fa-external-link', src: 'src/thirdparty/font-awesome-svg-png/white/svg/external-link.svg' },
|
{id: 'fa-search', src: 'node_modules/font-awesome-svg-png/white/svg/search.svg', title: 'Search'},
|
||||||
{ id: 'fa-search', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search.svg' },
|
{id: 'fa-comments', src: 'node_modules/font-awesome-svg-png/white/svg/comments.svg', title: 'Conversations'},
|
||||||
{ id: 'fa-comments', src: 'src/thirdparty/font-awesome-svg-png/white/svg/comments.svg' },
|
{id: 'fa-paperclip', src: 'node_modules/font-awesome-svg-png/white/svg/paperclip.svg', title: 'Paperclip'},
|
||||||
{ id: 'fa-paperclip', src: 'src/thirdparty/font-awesome-svg-png/white/svg/paperclip.svg' },
|
{id: 'fa-thumb-tack', src: 'node_modules/font-awesome-svg-png/white/svg/thumb-tack.svg', title: 'Thumbtack'},
|
||||||
{ id: 'fa-thumb-tack', src: 'src/thirdparty/font-awesome-svg-png/white/svg/thumb-tack.svg' },
|
{id: 'fa-bars', src: 'node_modules/font-awesome-svg-png/white/svg/bars.svg', title: 'List'},
|
||||||
{ id: 'fa-bars', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bars.svg' },
|
{id: 'fa-ban', src: 'node_modules/font-awesome-svg-png/white/svg/ban.svg', title: 'Ban'},
|
||||||
{ id: 'fa-ban', src: 'src/thirdparty/font-awesome-svg-png/white/svg/ban.svg' },
|
{id: 'fa-camera', src: 'node_modules/font-awesome-svg-png/white/svg/camera.svg', title: 'Add media'},
|
||||||
{ id: 'fa-camera', src: 'src/thirdparty/font-awesome-svg-png/white/svg/camera.svg' },
|
{id: 'fa-smile', src: 'node_modules/font-awesome-svg-png/white/svg/smile-o.svg', title: 'Custom emoji'},
|
||||||
{ id: 'fa-smile', src: 'src/thirdparty/font-awesome-svg-png/white/svg/smile-o.svg' },
|
{id: 'fa-exclamation-triangle', src: 'node_modules/font-awesome-svg-png/white/svg/exclamation-triangle.svg', title: 'Content warning'},
|
||||||
{ id: 'fa-exclamation-triangle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/exclamation-triangle.svg' },
|
{id: 'fa-check', src: 'node_modules/font-awesome-svg-png/white/svg/check.svg', title: 'Check'},
|
||||||
{ id: 'fa-check', src: 'src/thirdparty/font-awesome-svg-png/white/svg/check.svg' },
|
{id: 'fa-trash', src: 'node_modules/font-awesome-svg-png/white/svg/trash-o.svg', title: 'Delete'},
|
||||||
{ id: 'fa-trash', src: 'src/thirdparty/font-awesome-svg-png/white/svg/trash-o.svg' },
|
{id: 'fa-hourglass', src: 'node_modules/font-awesome-svg-png/white/svg/hourglass.svg', title: 'Follow requested'},
|
||||||
{ id: 'fa-hourglass', src: 'src/thirdparty/font-awesome-svg-png/white/svg/hourglass.svg' },
|
{id: 'fa-pencil', src: 'node_modules/font-awesome-svg-png/white/svg/pencil.svg', title: 'Compose'},
|
||||||
{ id: 'fa-pencil', src: 'src/thirdparty/font-awesome-svg-png/white/svg/pencil.svg' },
|
{id: 'fa-times', src: 'node_modules/font-awesome-svg-png/white/svg/times.svg', title: 'Close'},
|
||||||
{ id: 'fa-times', src: 'src/thirdparty/font-awesome-svg-png/white/svg/times.svg' },
|
{id: 'fa-volume-off', src: 'node_modules/font-awesome-svg-png/white/svg/volume-off.svg', title: 'Mute'},
|
||||||
{ id: 'fa-volume-off', src: 'src/thirdparty/font-awesome-svg-png/white/svg/volume-off.svg' },
|
{id: 'fa-volume-up', src: 'node_modules/font-awesome-svg-png/white/svg/volume-up.svg', title: 'Unmute'},
|
||||||
{ id: 'fa-volume-up', src: 'src/thirdparty/font-awesome-svg-png/white/svg/volume-up.svg' },
|
{id: 'fa-link', src: 'node_modules/font-awesome-svg-png/white/svg/link.svg', title: 'Link'}
|
||||||
{ id: 'fa-link', src: 'src/thirdparty/font-awesome-svg-png/white/svg/link.svg' },
|
|
||||||
{ id: 'fa-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/circle.svg' },
|
|
||||||
{ id: 'fa-circle-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/circle-o.svg' },
|
|
||||||
{ id: 'fa-angle-left', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-left.svg' },
|
|
||||||
{ id: 'fa-angle-right', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-right.svg' },
|
|
||||||
{ id: 'fa-search-minus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search-minus.svg' },
|
|
||||||
{ id: 'fa-search-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search-plus.svg' }
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import fetch from 'node-fetch'
|
import fetch from 'node-fetch'
|
||||||
import { actions } from './mastodon-data'
|
import { actions } from './mastodon-data'
|
||||||
|
|
||||||
const numStatuses = actions
|
const numStatuses = actions.filter(_ => _.post || _.boost).length
|
||||||
.map(_ => _.post || _.boost)
|
|
||||||
.filter(Boolean)
|
|
||||||
.filter(_ => _.privacy !== 'direct')
|
|
||||||
.length
|
|
||||||
|
|
||||||
async function waitForMastodonData () {
|
async function waitForMastodonData () {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
## Theming
|
## Theming
|
||||||
|
|
||||||
This document describes how to write your own theme for Pinafore.
|
Create a file `scss/themes/foobar.scss`, write some SCSS inside and add the following at the bottom of `scss/themes/foobar.scss`.
|
||||||
|
|
||||||
First, create a file `scss/themes/foobar.scss`, write some SCSS inside and add
|
|
||||||
the following at the bottom of `scss/themes/foobar.scss`.
|
|
||||||
```scss
|
```scss
|
||||||
@import "_base.scss";
|
@import "_base.scss";
|
||||||
|
|
||||||
|
@ -12,23 +9,50 @@ body.theme-foobar {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> Note: You can find all the SCSS variables available in `scss/themes/_default.scss`
|
> Note: You can find all the SCSS variables available in `scss/themes/_default.scss` while the all CSS Custom Properties available are listed in `scss/themes/_base.scss`.
|
||||||
> while the all CSS Custom Properties available are listed in `scss/themes/_base.scss`.
|
|
||||||
|
|
||||||
Then, Add your theme to `src/routes/_static/themes.js`
|
Add the CSS class you just define to `scss/themes/_offlines`.
|
||||||
|
```scss
|
||||||
|
...
|
||||||
|
body.offline,
|
||||||
|
body.theme-foobar.offline, // <-
|
||||||
|
body.theme-hotpants.offline,
|
||||||
|
body.theme-majesty.offline,
|
||||||
|
body.theme-oaken.offline,
|
||||||
|
body.theme-scarlet.offline,
|
||||||
|
body.theme-seafoam.offline,
|
||||||
|
body.theme-gecko.offline {
|
||||||
|
@include baseTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Add your theme to `routes/_static/themes.js`
|
||||||
```js
|
```js
|
||||||
const themes = [
|
const themes = [
|
||||||
...
|
...
|
||||||
{
|
{
|
||||||
name: 'foobar',
|
name: 'foobar',
|
||||||
label: 'Foobar', // user-visible name
|
label: 'Foobar'
|
||||||
color: 'magenta', // main theme color
|
|
||||||
dark: true // whether it's a dark theme or not
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export { themes }
|
||||||
```
|
```
|
||||||
|
|
||||||
Start the development server (`yarn run dev`), go to
|
Add your theme in `inline-script.js`.
|
||||||
`http://localhost:4002/settings/instances/your-instance-name` and select your
|
```js
|
||||||
newly-created theme. Once you've done that, you can update your theme, and refresh
|
window.__themeColors = {
|
||||||
the page to see the change (you don't have to restart the server).
|
'default': "royalblue",
|
||||||
|
scarlet: "#e04e41",
|
||||||
|
seafoam: "#177380",
|
||||||
|
hotpants: "hotpink",
|
||||||
|
oaken: "saddlebrown",
|
||||||
|
majesty: "blueviolet",
|
||||||
|
gecko: "#4ab92f",
|
||||||
|
foobar: "#BADA55", // <-
|
||||||
|
offline: "#999999"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the development server (`npm run dev`), go to `http://localhost:4002/settings/instances/your-instance-name` and select your newly created theme. Once you've done that, you can update your theme, and refresh the page to see the change (you don't have to restart the server).
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
// For perf reasons, this script is run inline to quickly set certain styles.
|
||||||
|
// To allow CSP to work correctly, we also calculate a sha256 hash during
|
||||||
|
// the build process and write it to inline-script-checksum.json.
|
||||||
|
window.__themeColors = {
|
||||||
|
'default': '#1ea21e',
|
||||||
|
royal: 'royalblue',
|
||||||
|
scarlet: '#e04e41',
|
||||||
|
seafoam: '#177380',
|
||||||
|
hotpants: 'hotpink',
|
||||||
|
oaken: 'saddlebrown',
|
||||||
|
majesty: 'blueviolet',
|
||||||
|
gecko: '#4ab92f',
|
||||||
|
ozark: '#5263af',
|
||||||
|
cobalt: '#08439b',
|
||||||
|
sorcery: '#ae91e8',
|
||||||
|
offline: '#999999'
|
||||||
|
}
|
||||||
|
if (localStorage.store_currentInstance && localStorage.store_instanceThemes) {
|
||||||
|
let safeParse = (str) => str === 'undefined' ? undefined : JSON.parse(str)
|
||||||
|
let theme = safeParse(localStorage.store_instanceThemes)[safeParse(localStorage.store_currentInstance)]
|
||||||
|
if (theme && theme !== 'default') {
|
||||||
|
document.body.classList.add(`theme-${theme}`)
|
||||||
|
let link = document.createElement('link')
|
||||||
|
link.rel = 'stylesheet'
|
||||||
|
link.href = `/theme-${theme}.css`
|
||||||
|
document.head.appendChild(link)
|
||||||
|
if (window.__themeColors[theme]) {
|
||||||
|
document.getElementById('theThemeColor').content = window.__themeColors[theme]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!localStorage.store_currentInstance) {
|
||||||
|
// if not logged in, show all these 'hidden-from-ssr' elements
|
||||||
|
let style = document.createElement('style')
|
||||||
|
style.textContent = '.hidden-from-ssr { opacity: 1 !important; }'
|
||||||
|
document.head.appendChild(style)
|
||||||
|
}
|
Before Width: | Height: | Size: 708 B After Width: | Height: | Size: 708 B |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 403 B After Width: | Height: | Size: 403 B |
Before Width: | Height: | Size: 326 B After Width: | Height: | Size: 326 B |
171
package.json
|
@ -1,25 +1,24 @@
|
||||||
{
|
{
|
||||||
"name": "pinafore",
|
"name": "pinafore",
|
||||||
"description": "Alternative web client for Mastodon",
|
"description": "Alternative web client for Mastodon",
|
||||||
"version": "1.0.1",
|
"version": "0.5.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "standard && standard --plugin html 'src/routes/**/*.html'",
|
"lint": "standard && standard --plugin html 'routes/**/*.html'",
|
||||||
"lint-fix": "standard --fix && standard --fix --plugin html 'src/routes/**/*.html'",
|
"lint-fix": "standard --fix && standard --fix --plugin html 'routes/**/*.html'",
|
||||||
"dev": "run-s build-template-html build-third-party-assets serve-dev",
|
"dev": "run-s build-svg build-inline-script serve-dev",
|
||||||
"serve-dev": "run-p --race build-template-html-watch sapper-dev",
|
"serve-dev": "run-p --race build-sass-watch serve",
|
||||||
"sapper-dev": "cross-env NODE_ENV=development PORT=4002 sapper dev",
|
"serve": "node server.js",
|
||||||
"sapper-prod": "cross-env PORT=4002 node __sapper__/build",
|
"build": "cross-env NODE_ENV=production npm run build-steps",
|
||||||
"before-build": "run-s build-template-html build-third-party-assets",
|
"build-steps": "run-s globalize-css build-sass build-svg build-inline-script sapper-build deglobalize-css",
|
||||||
"build": "cross-env NODE_ENV=production run-s build-steps",
|
|
||||||
"build-steps": "run-s before-build sapper-build",
|
|
||||||
"sapper-build": "sapper build",
|
"sapper-build": "sapper build",
|
||||||
"start": "cross-env NODE_ENV=production run-s sapper-prod",
|
"start": "cross-env NODE_ENV=production npm run serve",
|
||||||
"build-and-start": "run-s build start",
|
"build-and-start": "run-s build start",
|
||||||
"build-template-html": "node -r esm ./bin/build-template-html.js",
|
"build-svg": "node ./bin/build-svg.js",
|
||||||
"build-template-html-watch": "node -r esm ./bin/build-template-html.js --watch",
|
"build-inline-script": "node ./bin/build-inline-script.js",
|
||||||
"build-third-party-assets": "node -r esm ./bin/build-third-party-assets.js",
|
"build-sass": "node ./bin/build-sass.js",
|
||||||
|
"build-sass-watch": "node ./bin/build-sass.js --watch",
|
||||||
"run-mastodon": "node -r esm ./bin/run-mastodon.js",
|
"run-mastodon": "node -r esm ./bin/run-mastodon.js",
|
||||||
"test": "cross-env BROWSER=chrome:headless run-s test-browser",
|
"test": "cross-env BROWSER=chrome:headless npm run test-browser",
|
||||||
"test-browser": "run-p --race run-mastodon build-and-start test-mastodon",
|
"test-browser": "run-p --race run-mastodon build-and-start test-mastodon",
|
||||||
"test-mastodon": "run-s wait-for-mastodon-to-start wait-for-mastodon-data testcafe",
|
"test-mastodon": "run-s wait-for-mastodon-to-start wait-for-mastodon-data testcafe",
|
||||||
"test-browser-suite0": "run-p --race run-mastodon build-and-start test-mastodon-suite0",
|
"test-browser-suite0": "run-p --race run-mastodon build-and-start test-mastodon-suite0",
|
||||||
|
@ -29,87 +28,82 @@
|
||||||
"testcafe": "run-s testcafe-suite0 testcafe-suite1",
|
"testcafe": "run-s testcafe-suite0 testcafe-suite1",
|
||||||
"testcafe-suite0": "cross-env-shell testcafe --hostname localhost --skip-js-errors -c 4 $BROWSER tests/spec/0*",
|
"testcafe-suite0": "cross-env-shell testcafe --hostname localhost --skip-js-errors -c 4 $BROWSER tests/spec/0*",
|
||||||
"testcafe-suite1": "cross-env-shell testcafe --hostname localhost --skip-js-errors $BROWSER tests/spec/1*",
|
"testcafe-suite1": "cross-env-shell testcafe --hostname localhost --skip-js-errors $BROWSER tests/spec/1*",
|
||||||
"test-unit": "mocha -r esm tests/unit/",
|
|
||||||
"wait-for-mastodon-to-start": "node -r esm bin/wait-for-mastodon-to-start.js",
|
"wait-for-mastodon-to-start": "node -r esm bin/wait-for-mastodon-to-start.js",
|
||||||
"wait-for-mastodon-data": "node -r esm bin/wait-for-mastodon-data.js",
|
"wait-for-mastodon-data": "node -r esm bin/wait-for-mastodon-data.js",
|
||||||
"deploy-prod": "DEPLOY_TYPE=prod ./bin/deploy.sh",
|
"globalize-css": "node ./bin/globalize-css.js",
|
||||||
"deploy-dev": "DEPLOY_TYPE=dev ./bin/deploy.sh",
|
"deglobalize-css": "node ./bin/globalize-css.js --reverse",
|
||||||
"deploy-all-travis": "./bin/deploy-all-travis.sh",
|
"stage-dev": "printf 'User-agent: *\nDisallow: /' > assets/robots.txt",
|
||||||
"backup-mastodon-data": "./bin/backup-mastodon-data.sh",
|
"stage-prod": "rm -f assets/robots.txt",
|
||||||
"sapper-export": "sapper export",
|
"launch": "now -e SAPPER_TIMESTAMP=$(date +%s%3N) --team nolanlawson && sleep 60",
|
||||||
"print-export-info": "node ./bin/print-export-info.js",
|
"launch-travis": "now -e SAPPER_TIMESTAMP=$(date +%s%3N) --team nolanlawson --token $NOW_TOKEN && sleep 60",
|
||||||
"export-steps": "run-s before-build sapper-export print-export-info",
|
"alias-prod": "now alias pinafore.social --team nolanlawson",
|
||||||
"export": "cross-env NODE_ENV=production run-s export-steps"
|
"alias-dev": "now alias dev.pinafore.social --team nolanlawson",
|
||||||
|
"alias-dev-travis": "now alias dev.pinafore.social --team nolanlawson --token $NOW_TOKEN",
|
||||||
|
"cleanup": "now rm pinafore --safe --yes --team nolanlawson",
|
||||||
|
"cleanup-travis": "now rm pinafore --safe --yes --team nolanlawson --token $NOW_TOKEN",
|
||||||
|
"deploy-prod": "run-s stage-prod launch alias-prod cleanup",
|
||||||
|
"deploy-dev": "run-s stage-dev launch alias-dev cleanup",
|
||||||
|
"deploy-dev-travis": "if [ $TRAVIS_BRANCH = master -a $TRAVIS_PULL_REQUEST = false ]; then run-s stage-dev launch-travis alias-dev-travis cleanup-travis; fi",
|
||||||
|
"backup-mastodon-data": "PGPASSWORD=pinafore pg_dump -U pinafore -w mastodon_development > fixtures/dump.sql && cd mastodon/public/system && tar -czf ../../../fixtures/system.tgz ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gamestdio/websocket": "^0.2.8",
|
"@gamestdio/websocket": "^0.2.7",
|
||||||
"@webcomponents/custom-elements": "^1.2.1",
|
"a11y-dialog": "^4.0.1",
|
||||||
|
"browserslist": "^4.0.2",
|
||||||
"cheerio": "^1.0.0-rc.2",
|
"cheerio": "^1.0.0-rc.2",
|
||||||
"child-process-promise": "^2.2.1",
|
"child-process-promise": "^2.2.1",
|
||||||
"chokidar": "^2.0.4",
|
"chokidar": "^2.0.4",
|
||||||
"circular-dependency-plugin": "^5.0.2",
|
|
||||||
"clean-css": "^4.2.1",
|
|
||||||
"compression": "^1.7.3",
|
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"css-dedoupe": "^0.1.1",
|
"css-loader": "^1.0.0",
|
||||||
"css-loader": "^2.1.0",
|
|
||||||
"emoji-mart": "github:nolanlawson/emoji-mart#for-pinafore-1",
|
|
||||||
"emoji-regex": "^7.0.3",
|
|
||||||
"encoding": "^0.1.12",
|
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"esm": "^3.1.4",
|
"esm": "^3.0.77",
|
||||||
"events-light": "^1.0.5",
|
"events": "^3.0.0",
|
||||||
"express": "^4.16.4",
|
"express": "^4.16.3",
|
||||||
|
"fg-loadcss": "^2.0.1",
|
||||||
"file-api": "^0.10.4",
|
"file-api": "^0.10.4",
|
||||||
"file-drop-element": "0.0.9",
|
"font-awesome-svg-png": "^1.2.2",
|
||||||
"form-data": "^2.3.3",
|
"form-data": "^2.3.2",
|
||||||
"glob": "^7.1.3",
|
"glob": "^7.1.2",
|
||||||
"helmet": "^3.15.0",
|
"helmet": "^3.13.0",
|
||||||
"idb-keyval": "^3.1.0",
|
|
||||||
"indexeddb-getall-shim": "^1.3.5",
|
"indexeddb-getall-shim": "^1.3.5",
|
||||||
"inferno-compat": "^7.1.0",
|
"intersection-observer": "^0.5.0",
|
||||||
"intersection-observer": "^0.5.1",
|
"lodash-es": "^4.17.10",
|
||||||
"localstorage-memory": "^1.0.3",
|
|
||||||
"lodash-es": "^4.17.11",
|
|
||||||
"lodash-webpack-plugin": "^0.11.5",
|
"lodash-webpack-plugin": "^0.11.5",
|
||||||
|
"mini-css-extract-plugin": "^0.4.1",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"node-fetch": "^2.3.0",
|
"node-fetch": "^2.2.0",
|
||||||
"node-sass": "^4.11.0",
|
"node-sass": "^4.9.3",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.3",
|
||||||
|
"optimize-css-assets-webpack-plugin": "^5.0.0",
|
||||||
"p-any": "^1.1.0",
|
"p-any": "^1.1.0",
|
||||||
"page-lifecycle": "^0.1.1",
|
"page-lifecycle": "^0.1.1",
|
||||||
"performance-now": "^2.1.0",
|
"performance-now": "^2.1.0",
|
||||||
"pinch-zoom-element": "^1.1.0",
|
"pify": "^4.0.0",
|
||||||
"prop-types": "^15.6.2",
|
"quick-lru": "^1.1.0",
|
||||||
"quick-lru": "^2.0.0",
|
|
||||||
"remount": "^0.9.3",
|
|
||||||
"requestidlecallback": "^0.3.0",
|
"requestidlecallback": "^0.3.0",
|
||||||
"rollup": "^1.1.2",
|
"sapper": "github:nolanlawson/sapper#for-pinafore-7",
|
||||||
"rollup-plugin-replace": "^2.1.0",
|
|
||||||
"rollup-plugin-terser": "^4.0.3",
|
|
||||||
"sapper": "^0.25.0",
|
|
||||||
"serve-static": "^1.13.2",
|
"serve-static": "^1.13.2",
|
||||||
|
"shrink-ray-current": "^2.1.2",
|
||||||
"stringz": "^1.0.0",
|
"stringz": "^1.0.0",
|
||||||
"svelte": "^2.16.0",
|
"style-loader": "^0.22.1",
|
||||||
|
"svelte": "^2.11.0",
|
||||||
"svelte-extras": "^2.0.2",
|
"svelte-extras": "^2.0.2",
|
||||||
"svelte-loader": "^2.12.0",
|
"svelte-loader": "^2.10.1",
|
||||||
"svelte-transitions": "^1.2.0",
|
"svelte-transitions": "^1.2.0",
|
||||||
"svgo": "^1.1.1",
|
"svgo": "^1.0.5",
|
||||||
"terser-webpack-plugin": "^1.2.1",
|
"timeago.js": "^3.0.2",
|
||||||
"text-encoding": "^0.7.0",
|
|
||||||
"tiny-queue": "^0.2.1",
|
"tiny-queue": "^0.2.1",
|
||||||
"uuid": "^3.3.2",
|
"uglifyjs-webpack-plugin": "^1.3.0",
|
||||||
"web-animations-js": "^2.3.1",
|
"web-animations-js": "^2.3.1",
|
||||||
"webpack": "^4.29.0",
|
"webpack": "^4.16.5",
|
||||||
"webpack-bundle-analyzer": "^3.0.3"
|
"webpack-bundle-analyzer": "^2.13.1",
|
||||||
|
"yargs": "^12.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"assert": "^1.4.1",
|
"eslint-plugin-html": "^4.0.5",
|
||||||
"eslint-plugin-html": "^5.0.0",
|
"now": "^11.3.10",
|
||||||
"mocha": "^5.2.0",
|
"standard": "^11.0.1",
|
||||||
"now": "^13.1.2",
|
"testcafe": "^0.21.1"
|
||||||
"standard": "^12.0.1",
|
|
||||||
"testcafe": "^1.0.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
|
@ -142,18 +136,12 @@
|
||||||
"btoa",
|
"btoa",
|
||||||
"Blob",
|
"Blob",
|
||||||
"Element",
|
"Element",
|
||||||
"Image",
|
"Image"
|
||||||
"NotificationEvent",
|
|
||||||
"NodeList",
|
|
||||||
"DOMParser",
|
|
||||||
"CSS",
|
|
||||||
"customElements"
|
|
||||||
],
|
],
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"dist",
|
"dist",
|
||||||
"src/routes/_utils/asyncModules.js",
|
"routes/_utils/asyncModules.js",
|
||||||
"src/routes/_utils/asyncPolyfills.js",
|
"routes/_components/dialog/asyncDialogs.js"
|
||||||
"src/routes/_components/dialog/asyncDialogs.js"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"esm": {
|
"esm": {
|
||||||
|
@ -166,26 +154,27 @@
|
||||||
"NODE_ENV": "production"
|
"NODE_ENV": "production"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
"assets",
|
||||||
"bin",
|
"bin",
|
||||||
"inline-script.js",
|
"original-assets",
|
||||||
"original-static",
|
"routes",
|
||||||
"scss",
|
"scss",
|
||||||
"src",
|
"templates",
|
||||||
"src-build",
|
|
||||||
"static",
|
|
||||||
"package.json",
|
"package.json",
|
||||||
"thirdparty",
|
"package-lock.json",
|
||||||
"webpack",
|
"server.js",
|
||||||
"webpack.config.js",
|
"inline-script.js",
|
||||||
"yarn.lock"
|
"webpack.client.config.js",
|
||||||
|
"webpack.server.config.js"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10.0.0"
|
"node": "^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"greenkeeper": {
|
"greenkeeper": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"sapper"
|
"sapper",
|
||||||
|
"a11y-dialog"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { getAccount, getRelationship } from '../_api/user'
|
||||||
|
import {
|
||||||
|
getAccount as getAccountFromDatabase,
|
||||||
|
setAccount as setAccountInDatabase,
|
||||||
|
getRelationship as getRelationshipFromDatabase,
|
||||||
|
setRelationship as setRelationshipInDatabase
|
||||||
|
} from '../_database/accountsAndRelationships'
|
||||||
|
import { store } from '../_store/store'
|
||||||
|
|
||||||
|
async function updateAccount (accountId, instanceName, accessToken) {
|
||||||
|
let localPromise = getAccountFromDatabase(instanceName, accountId)
|
||||||
|
let remotePromise = getAccount(instanceName, accessToken, accountId).then(account => {
|
||||||
|
/* no await */ setAccountInDatabase(instanceName, account)
|
||||||
|
return account
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
store.set({currentAccountProfile: (await localPromise)})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
store.set({currentAccountProfile: (await remotePromise)})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateRelationship (accountId, instanceName, accessToken) {
|
||||||
|
let localPromise = getRelationshipFromDatabase(instanceName, accountId)
|
||||||
|
let remotePromise = getRelationship(instanceName, accessToken, accountId).then(relationship => {
|
||||||
|
/* no await */ setRelationshipInDatabase(instanceName, relationship)
|
||||||
|
return relationship
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
store.set({currentAccountRelationship: (await localPromise)})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
store.set({currentAccountRelationship: (await remotePromise)})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearProfileAndRelationship () {
|
||||||
|
store.set({
|
||||||
|
currentAccountProfile: null,
|
||||||
|
currentAccountRelationship: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateProfileAndRelationship (accountId) {
|
||||||
|
let { currentInstance, accessToken } = store.get()
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
updateAccount(accountId, currentInstance, accessToken),
|
||||||
|
updateRelationship(accountId, currentInstance, accessToken)
|
||||||
|
])
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
import { getAccessTokenFromAuthCode, registerApplication, generateAuthLink } from '../_api/oauth'
|
import { getAccessTokenFromAuthCode, registerApplication, generateAuthLink } from '../_api/oauth'
|
||||||
import { getInstanceInfo } from '../_api/instance'
|
import { getInstanceInfo } from '../_api/instance'
|
||||||
import { goto } from '../../../__sapper__/client'
|
import { goto } from 'sapper/runtime.js'
|
||||||
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine'
|
import { switchToTheme } from '../_utils/themeEngine'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { updateVerifyCredentialsForInstance } from './instances'
|
import { updateVerifyCredentialsForInstance } from './instances'
|
||||||
import { updateCustomEmojiForInstance } from './emoji'
|
import { updateCustomEmojiForInstance } from './emoji'
|
||||||
import { database } from '../_database/database'
|
import { setInstanceInfo as setInstanceInfoInDatabase } from '../_database/meta'
|
||||||
|
|
||||||
const REDIRECT_URI = (typeof location !== 'undefined'
|
const REDIRECT_URI = (typeof location !== 'undefined'
|
||||||
? location.origin : 'https://pinafore.social') + '/settings/instances/add'
|
? location.origin : 'https://pinafore.social') + '/settings/instances/add'
|
||||||
|
@ -14,13 +14,12 @@ async function redirectToOauth () {
|
||||||
let { instanceNameInSearch, loggedInInstances } = store.get()
|
let { instanceNameInSearch, loggedInInstances } = store.get()
|
||||||
instanceNameInSearch = instanceNameInSearch.replace(/^https?:\/\//, '').replace(/\/$/, '').replace('/$', '').toLowerCase()
|
instanceNameInSearch = instanceNameInSearch.replace(/^https?:\/\//, '').replace(/\/$/, '').replace('/$', '').toLowerCase()
|
||||||
if (Object.keys(loggedInInstances).includes(instanceNameInSearch)) {
|
if (Object.keys(loggedInInstances).includes(instanceNameInSearch)) {
|
||||||
let err = new Error(`You've already logged in to ${instanceNameInSearch}`)
|
store.set({logInToInstanceError: `You've already logged in to ${instanceNameInSearch}`})
|
||||||
err.knownError = true
|
return
|
||||||
throw err
|
|
||||||
}
|
}
|
||||||
let registrationPromise = registerApplication(instanceNameInSearch, REDIRECT_URI)
|
let registrationPromise = registerApplication(instanceNameInSearch, REDIRECT_URI)
|
||||||
let instanceInfo = await getInstanceInfo(instanceNameInSearch)
|
let instanceInfo = await getInstanceInfo(instanceNameInSearch)
|
||||||
await database.setInstanceInfo(instanceNameInSearch, instanceInfo) // cache for later
|
await setInstanceInfoInDatabase(instanceNameInSearch, instanceInfo) // cache for later
|
||||||
let instanceData = await registrationPromise
|
let instanceData = await registrationPromise
|
||||||
store.set({
|
store.set({
|
||||||
currentRegisteredInstanceName: instanceNameInSearch,
|
currentRegisteredInstanceName: instanceNameInSearch,
|
||||||
|
@ -45,17 +44,16 @@ export async function logInToInstance () {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
let error = `${err.message || err.name}. ` +
|
let error = `${err.message || err.name}. ` +
|
||||||
(err.knownError ? '' : (navigator.onLine
|
(navigator.onLine
|
||||||
? `Is this a valid Mastodon instance? Is a browser extension
|
? `Is this a valid Mastodon instance? Is a browser extension blocking the request?`
|
||||||
blocking the request? Are you in private browsing mode?`
|
: `Are you offline?`)
|
||||||
: `Are you offline?`))
|
|
||||||
let { instanceNameInSearch } = store.get()
|
let { instanceNameInSearch } = store.get()
|
||||||
store.set({
|
store.set({
|
||||||
logInToInstanceError: error,
|
logInToInstanceError: error,
|
||||||
logInToInstanceErrorForText: instanceNameInSearch
|
logInToInstanceErrorForText: instanceNameInSearch
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
store.set({ logInToInstanceLoading: false })
|
store.set({logInToInstanceLoading: false})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +67,7 @@ async function registerNewInstance (code) {
|
||||||
REDIRECT_URI
|
REDIRECT_URI
|
||||||
)
|
)
|
||||||
let { loggedInInstances, loggedInInstancesInOrder, instanceThemes } = store.get()
|
let { loggedInInstances, loggedInInstancesInOrder, instanceThemes } = store.get()
|
||||||
instanceThemes[currentRegisteredInstanceName] = DEFAULT_THEME
|
instanceThemes[currentRegisteredInstanceName] = 'default'
|
||||||
loggedInInstances[currentRegisteredInstanceName] = instanceData
|
loggedInInstances[currentRegisteredInstanceName] = instanceData
|
||||||
if (!loggedInInstancesInOrder.includes(currentRegisteredInstanceName)) {
|
if (!loggedInInstancesInOrder.includes(currentRegisteredInstanceName)) {
|
||||||
loggedInInstancesInOrder.push(currentRegisteredInstanceName)
|
loggedInInstancesInOrder.push(currentRegisteredInstanceName)
|
||||||
|
@ -84,7 +82,7 @@ async function registerNewInstance (code) {
|
||||||
instanceThemes: instanceThemes
|
instanceThemes: instanceThemes
|
||||||
})
|
})
|
||||||
store.save()
|
store.save()
|
||||||
switchToTheme(DEFAULT_THEME)
|
switchToTheme('default')
|
||||||
// fire off these requests so they're cached
|
// fire off these requests so they're cached
|
||||||
/* no await */ updateVerifyCredentialsForInstance(currentRegisteredInstanceName)
|
/* no await */ updateVerifyCredentialsForInstance(currentRegisteredInstanceName)
|
||||||
/* no await */ updateCustomEmojiForInstance(currentRegisteredInstanceName)
|
/* no await */ updateCustomEmojiForInstance(currentRegisteredInstanceName)
|
||||||
|
@ -93,11 +91,11 @@ async function registerNewInstance (code) {
|
||||||
|
|
||||||
export async function handleOauthCode (code) {
|
export async function handleOauthCode (code) {
|
||||||
try {
|
try {
|
||||||
store.set({ logInToInstanceLoading: true })
|
store.set({logInToInstanceLoading: true})
|
||||||
await registerNewInstance(code)
|
await registerNewInstance(code)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
store.set({ logInToInstanceError: `${err.message || err.name}. Failed to connect to instance.` })
|
store.set({logInToInstanceError: `${err.message || err.name}. Failed to connect to instance.`})
|
||||||
} finally {
|
} finally {
|
||||||
store.set({ logInToInstanceLoading: false })
|
store.set({logInToInstanceLoading: false})
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,15 @@
|
||||||
|
import throttle from 'lodash-es/throttle'
|
||||||
import { mark, stop } from '../_utils/marks'
|
import { mark, stop } from '../_utils/marks'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import uniqBy from 'lodash-es/uniqBy'
|
import uniqBy from 'lodash-es/uniqBy'
|
||||||
import uniq from 'lodash-es/uniq'
|
import uniq from 'lodash-es/uniq'
|
||||||
import isEqual from 'lodash-es/isEqual'
|
import isEqual from 'lodash-es/isEqual'
|
||||||
import { database } from '../_database/database'
|
import {
|
||||||
import { concat } from '../_utils/arrays'
|
insertTimelineItems as insertTimelineItemsInDatabase
|
||||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
} from '../_database/timelines/insertion'
|
||||||
|
import { runMediumPriorityTask } from '../_utils/runMediumPriorityTask'
|
||||||
|
|
||||||
|
const STREAMING_THROTTLE_DELAY = 3000
|
||||||
|
|
||||||
function getExistingItemIdsSet (instanceName, timelineName) {
|
function getExistingItemIdsSet (instanceName, timelineName) {
|
||||||
let timelineItemIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds') || []
|
let timelineItemIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds') || []
|
||||||
|
@ -25,31 +29,14 @@ async function insertUpdatesIntoTimeline (instanceName, timelineName, updates) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await database.insertTimelineItems(instanceName, timelineName, updates)
|
await insertTimelineItemsInDatabase(instanceName, timelineName, updates)
|
||||||
|
|
||||||
let itemIdsToAdd = store.getForTimeline(instanceName, timelineName, 'itemIdsToAdd') || []
|
let itemIdsToAdd = store.getForTimeline(instanceName, timelineName, 'itemIdsToAdd') || []
|
||||||
let newItemIdsToAdd = uniq(concat(itemIdsToAdd, updates.map(_ => _.id)))
|
let newItemIdsToAdd = uniq([].concat(itemIdsToAdd).concat(updates.map(_ => _.id)))
|
||||||
if (!isEqual(itemIdsToAdd, newItemIdsToAdd)) {
|
if (!isEqual(itemIdsToAdd, newItemIdsToAdd)) {
|
||||||
console.log('adding ', (newItemIdsToAdd.length - itemIdsToAdd.length),
|
console.log('adding ', (newItemIdsToAdd.length - itemIdsToAdd.length),
|
||||||
'items to itemIdsToAdd for timeline', timelineName)
|
'items to itemIdsToAdd for timeline', timelineName)
|
||||||
store.setForTimeline(instanceName, timelineName, { itemIdsToAdd: newItemIdsToAdd })
|
store.setForTimeline(instanceName, timelineName, {itemIdsToAdd: newItemIdsToAdd})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidStatusForThread (thread, timelineName, itemIdsToAdd) {
|
|
||||||
let focusedStatusId = timelineName.split('/')[1] // e.g. "status/123456"
|
|
||||||
let focusedStatusIdx = thread.indexOf(focusedStatusId)
|
|
||||||
return status => {
|
|
||||||
let repliedToStatusIdx = thread.indexOf(status.in_reply_to_id)
|
|
||||||
return (
|
|
||||||
// A reply to an ancestor status is not valid for this thread, but for the focused status
|
|
||||||
// itself or any of its descendents, it is valid.
|
|
||||||
repliedToStatusIdx >= focusedStatusIdx &&
|
|
||||||
// Not a duplicate
|
|
||||||
!thread.includes(status.id) &&
|
|
||||||
// Not already about to be added
|
|
||||||
!itemIdsToAdd.includes(status.id)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,20 +46,21 @@ async function insertUpdatesIntoThreads (instanceName, updates) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let threads = store.getThreads(instanceName)
|
let threads = store.getThreads(instanceName)
|
||||||
let timelineNames = Object.keys(threads)
|
|
||||||
for (let timelineName of timelineNames) {
|
|
||||||
let thread = threads[timelineName]
|
|
||||||
|
|
||||||
let itemIdsToAdd = store.getForTimeline(instanceName, timelineName, 'itemIdsToAdd') || []
|
for (let timelineName of Object.keys(threads)) {
|
||||||
let validUpdates = updates.filter(isValidStatusForThread(thread, timelineName, itemIdsToAdd))
|
let thread = threads[timelineName]
|
||||||
if (!validUpdates.length) {
|
let updatesForThisThread = updates.filter(
|
||||||
|
status => thread.includes(status.in_reply_to_id) && !thread.includes(status.id)
|
||||||
|
)
|
||||||
|
if (!updatesForThisThread.length) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let newItemIdsToAdd = uniq(concat(itemIdsToAdd, validUpdates.map(_ => _.id)))
|
let itemIdsToAdd = store.getForTimeline(instanceName, timelineName, 'itemIdsToAdd') || []
|
||||||
|
let newItemIdsToAdd = uniq([].concat(itemIdsToAdd).concat(updatesForThisThread.map(_ => _.id)))
|
||||||
if (!isEqual(itemIdsToAdd, newItemIdsToAdd)) {
|
if (!isEqual(itemIdsToAdd, newItemIdsToAdd)) {
|
||||||
console.log('adding ', (newItemIdsToAdd.length - itemIdsToAdd.length),
|
console.log('adding ', (newItemIdsToAdd.length - itemIdsToAdd.length),
|
||||||
'items to itemIdsToAdd for thread', timelineName)
|
'items to itemIdsToAdd for thread', timelineName)
|
||||||
store.setForTimeline(instanceName, timelineName, { itemIdsToAdd: newItemIdsToAdd })
|
store.setForTimeline(instanceName, timelineName, {itemIdsToAdd: newItemIdsToAdd})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +70,7 @@ async function processFreshUpdates (instanceName, timelineName) {
|
||||||
let freshUpdates = store.getForTimeline(instanceName, timelineName, 'freshUpdates')
|
let freshUpdates = store.getForTimeline(instanceName, timelineName, 'freshUpdates')
|
||||||
if (freshUpdates && freshUpdates.length) {
|
if (freshUpdates && freshUpdates.length) {
|
||||||
let updates = freshUpdates.slice()
|
let updates = freshUpdates.slice()
|
||||||
store.setForTimeline(instanceName, timelineName, { freshUpdates: [] })
|
store.setForTimeline(instanceName, timelineName, {freshUpdates: []})
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
insertUpdatesIntoTimeline(instanceName, timelineName, updates),
|
insertUpdatesIntoTimeline(instanceName, timelineName, updates),
|
||||||
|
@ -92,11 +80,11 @@ async function processFreshUpdates (instanceName, timelineName) {
|
||||||
stop('processFreshUpdates')
|
stop('processFreshUpdates')
|
||||||
}
|
}
|
||||||
|
|
||||||
function lazilyProcessFreshUpdates (instanceName, timelineName) {
|
const lazilyProcessFreshUpdates = throttle((instanceName, timelineName) => {
|
||||||
scheduleIdleTask(() => {
|
runMediumPriorityTask(() => {
|
||||||
/* no await */ processFreshUpdates(instanceName, timelineName)
|
/* no await */ processFreshUpdates(instanceName, timelineName)
|
||||||
})
|
})
|
||||||
}
|
}, STREAMING_THROTTLE_DELAY)
|
||||||
|
|
||||||
export function addStatusOrNotification (instanceName, timelineName, newStatusOrNotification) {
|
export function addStatusOrNotification (instanceName, timelineName, newStatusOrNotification) {
|
||||||
addStatusesOrNotifications(instanceName, timelineName, [newStatusOrNotification])
|
addStatusesOrNotifications(instanceName, timelineName, [newStatusOrNotification])
|
||||||
|
@ -105,8 +93,8 @@ export function addStatusOrNotification (instanceName, timelineName, newStatusOr
|
||||||
export function addStatusesOrNotifications (instanceName, timelineName, newStatusesOrNotifications) {
|
export function addStatusesOrNotifications (instanceName, timelineName, newStatusesOrNotifications) {
|
||||||
console.log('addStatusesOrNotifications', Date.now())
|
console.log('addStatusesOrNotifications', Date.now())
|
||||||
let freshUpdates = store.getForTimeline(instanceName, timelineName, 'freshUpdates') || []
|
let freshUpdates = store.getForTimeline(instanceName, timelineName, 'freshUpdates') || []
|
||||||
freshUpdates = concat(freshUpdates, newStatusesOrNotifications)
|
freshUpdates = [].concat(freshUpdates).concat(newStatusesOrNotifications)
|
||||||
freshUpdates = uniqBy(freshUpdates, _ => _.id)
|
freshUpdates = uniqBy(freshUpdates, _ => _.id)
|
||||||
store.setForTimeline(instanceName, timelineName, { freshUpdates: freshUpdates })
|
store.setForTimeline(instanceName, timelineName, {freshUpdates: freshUpdates})
|
||||||
lazilyProcessFreshUpdates(instanceName, timelineName)
|
lazilyProcessFreshUpdates(instanceName, timelineName)
|
||||||
}
|
}
|
|
@ -6,8 +6,8 @@ export async function insertUsername (realm, username, startIndex, endIndex) {
|
||||||
let pre = oldText.substring(0, startIndex)
|
let pre = oldText.substring(0, startIndex)
|
||||||
let post = oldText.substring(endIndex)
|
let post = oldText.substring(endIndex)
|
||||||
let newText = `${pre}@${username} ${post}`
|
let newText = `${pre}@${username} ${post}`
|
||||||
store.setComposeData(realm, { text: newText })
|
store.setComposeData(realm, {text: newText})
|
||||||
store.setForAutosuggest(currentInstance, realm, { autosuggestSearchResults: [] })
|
store.setForAutosuggest(currentInstance, realm, {autosuggestSearchResults: []})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clickSelectedAutosuggestionUsername (realm) {
|
export async function clickSelectedAutosuggestionUsername (realm) {
|
||||||
|
@ -29,8 +29,8 @@ export function insertEmojiAtPosition (realm, emoji, startIndex, endIndex) {
|
||||||
let pre = oldText.substring(0, startIndex)
|
let pre = oldText.substring(0, startIndex)
|
||||||
let post = oldText.substring(endIndex)
|
let post = oldText.substring(endIndex)
|
||||||
let newText = `${pre}:${emoji.shortcode}: ${post}`
|
let newText = `${pre}:${emoji.shortcode}: ${post}`
|
||||||
store.setComposeData(realm, { text: newText })
|
store.setComposeData(realm, {text: newText})
|
||||||
store.setForAutosuggest(currentInstance, realm, { autosuggestSearchResults: [] })
|
store.setForAutosuggest(currentInstance, realm, {autosuggestSearchResults: []})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clickSelectedAutosuggestionEmoji (realm) {
|
export async function clickSelectedAutosuggestionEmoji (realm) {
|
|
@ -1,19 +1,18 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { blockAccount, unblockAccount } from '../_api/block'
|
import { blockAccount, unblockAccount } from '../_api/block'
|
||||||
import { toast } from '../_components/toast/toast'
|
import { toast } from '../_utils/toast'
|
||||||
import { updateLocalRelationship } from './accounts'
|
import { updateProfileAndRelationship } from './accounts'
|
||||||
import { emit } from '../_utils/eventBus'
|
import { emit } from '../_utils/eventBus'
|
||||||
|
|
||||||
export async function setAccountBlocked (accountId, block, toastOnSuccess) {
|
export async function setAccountBlocked (accountId, block, toastOnSuccess) {
|
||||||
let { currentInstance, accessToken } = store.get()
|
let { currentInstance, accessToken } = store.get()
|
||||||
try {
|
try {
|
||||||
let relationship
|
|
||||||
if (block) {
|
if (block) {
|
||||||
relationship = await blockAccount(currentInstance, accessToken, accountId)
|
await blockAccount(currentInstance, accessToken, accountId)
|
||||||
} else {
|
} else {
|
||||||
relationship = await unblockAccount(currentInstance, accessToken, accountId)
|
await unblockAccount(currentInstance, accessToken, accountId)
|
||||||
}
|
}
|
||||||
await updateLocalRelationship(currentInstance, accountId, relationship)
|
await updateProfileAndRelationship(accountId)
|
||||||
if (toastOnSuccess) {
|
if (toastOnSuccess) {
|
||||||
if (block) {
|
if (block) {
|
||||||
toast.say('Blocked account')
|
toast.say('Blocked account')
|
|
@ -1,14 +1,14 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { toast } from '../_components/toast/toast'
|
import { toast } from '../_utils/toast'
|
||||||
import { postStatus as postStatusToServer } from '../_api/statuses'
|
import { postStatus as postStatusToServer } from '../_api/statuses'
|
||||||
import { addStatusOrNotification } from './addStatusOrNotification'
|
import { addStatusOrNotification } from './addStatusOrNotification'
|
||||||
import { database } from '../_database/database'
|
import { getStatus as getStatusFromDatabase } from '../_database/timelines/getStatusOrNotification'
|
||||||
import { emit } from '../_utils/eventBus'
|
import { emit } from '../_utils/eventBus'
|
||||||
import { putMediaDescription } from '../_api/media'
|
import { putMediaDescription } from '../_api/media'
|
||||||
|
|
||||||
export async function insertHandleForReply (statusId) {
|
export async function insertHandleForReply (statusId) {
|
||||||
let { currentInstance } = store.get()
|
let { currentInstance } = store.get()
|
||||||
let status = await database.getStatus(currentInstance, statusId)
|
let status = await getStatusFromDatabase(currentInstance, statusId)
|
||||||
let { currentVerifyCredentials } = store.get()
|
let { currentVerifyCredentials } = store.get()
|
||||||
let originalStatus = status.reblog || status
|
let originalStatus = status.reblog || status
|
||||||
let accounts = [originalStatus.account].concat(originalStatus.mentions || [])
|
let accounts = [originalStatus.account].concat(originalStatus.mentions || [])
|
||||||
|
@ -22,7 +22,7 @@ export async function insertHandleForReply (statusId) {
|
||||||
|
|
||||||
export async function postStatus (realm, text, inReplyToId, mediaIds,
|
export async function postStatus (realm, text, inReplyToId, mediaIds,
|
||||||
sensitive, spoilerText, visibility,
|
sensitive, spoilerText, visibility,
|
||||||
mediaDescriptions, inReplyToUuid) {
|
mediaDescriptions = [], inReplyToUuid) {
|
||||||
let { currentInstance, accessToken, online } = store.get()
|
let { currentInstance, accessToken, online } = store.get()
|
||||||
|
|
||||||
if (!online) {
|
if (!online) {
|
||||||
|
@ -30,9 +30,6 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
text = text || ''
|
|
||||||
mediaDescriptions = mediaDescriptions || []
|
|
||||||
|
|
||||||
store.set({
|
store.set({
|
||||||
postingStatus: true
|
postingStatus: true
|
||||||
})
|
})
|
||||||
|
@ -49,7 +46,7 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast.say('Unable to post status: ' + (e.message || ''))
|
toast.say('Unable to post status: ' + (e.message || ''))
|
||||||
} finally {
|
} finally {
|
||||||
store.set({ postingStatus: false })
|
store.set({postingStatus: false})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,5 +81,5 @@ export function setReplyVisibility (realm, replyVisibility) {
|
||||||
let visibility = PRIVACY_LEVEL[replyVisibility] < PRIVACY_LEVEL[defaultVisibility]
|
let visibility = PRIVACY_LEVEL[replyVisibility] < PRIVACY_LEVEL[defaultVisibility]
|
||||||
? replyVisibility
|
? replyVisibility
|
||||||
: defaultVisibility
|
: defaultVisibility
|
||||||
store.setComposeData(realm, { postPrivacy: visibility })
|
store.setComposeData(realm, {postPrivacy: visibility})
|
||||||
}
|
}
|
|
@ -1,13 +1,11 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { deleteStatus } from '../_api/delete'
|
import { deleteStatus } from '../_api/delete'
|
||||||
import { toast } from '../_components/toast/toast'
|
import { toast } from '../_utils/toast'
|
||||||
import { deleteStatus as deleteStatusLocally } from './deleteStatuses'
|
|
||||||
|
|
||||||
export async function doDeleteStatus (statusId) {
|
export async function doDeleteStatus (statusId) {
|
||||||
let { currentInstance, accessToken } = store.get()
|
let { currentInstance, accessToken } = store.get()
|
||||||
try {
|
try {
|
||||||
await deleteStatus(currentInstance, accessToken, statusId)
|
await deleteStatus(currentInstance, accessToken, statusId)
|
||||||
deleteStatusLocally(currentInstance, statusId)
|
|
||||||
toast.say('Status deleted.')
|
toast.say('Status deleted.')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
|
@ -1,8 +1,10 @@
|
||||||
import { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses'
|
import { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import isEqual from 'lodash-es/isEqual'
|
|
||||||
import { database } from '../_database/database'
|
|
||||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||||
|
import isEqual from 'lodash-es/isEqual'
|
||||||
|
import {
|
||||||
|
deleteStatusesAndNotifications as deleteStatusesAndNotificationsFromDatabase
|
||||||
|
} from '../_database/timelines/deletion'
|
||||||
|
|
||||||
function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) {
|
function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) {
|
||||||
let keys = ['timelineItemIds', 'itemIdsToAdd']
|
let keys = ['timelineItemIds', 'itemIdsToAdd']
|
||||||
|
@ -16,7 +18,6 @@ function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) {
|
||||||
}
|
}
|
||||||
let filteredIds = ids.filter(idFilter)
|
let filteredIds = ids.filter(idFilter)
|
||||||
if (!isEqual(ids, filteredIds)) {
|
if (!isEqual(ids, filteredIds)) {
|
||||||
console.log('deleting an item from timelineName', timelineName, 'for key', key)
|
|
||||||
store.setForTimeline(instanceName, timelineName, {
|
store.setForTimeline(instanceName, timelineName, {
|
||||||
[key]: filteredIds
|
[key]: filteredIds
|
||||||
})
|
})
|
||||||
|
@ -44,7 +45,7 @@ function deleteNotificationIdsFromStore (instanceName, idsToDelete) {
|
||||||
async function deleteStatusesAndNotifications (instanceName, statusIdsToDelete, notificationIdsToDelete) {
|
async function deleteStatusesAndNotifications (instanceName, statusIdsToDelete, notificationIdsToDelete) {
|
||||||
deleteStatusIdsFromStore(instanceName, statusIdsToDelete)
|
deleteStatusIdsFromStore(instanceName, statusIdsToDelete)
|
||||||
deleteNotificationIdsFromStore(instanceName, notificationIdsToDelete)
|
deleteNotificationIdsFromStore(instanceName, notificationIdsToDelete)
|
||||||
await database.deleteStatusesAndNotifications(instanceName, statusIdsToDelete, notificationIdsToDelete)
|
await deleteStatusesAndNotificationsFromDatabase(instanceName, statusIdsToDelete, notificationIdsToDelete)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doDeleteStatus (instanceName, statusId) {
|
async function doDeleteStatus (instanceName, statusId) {
|
|
@ -1,28 +1,30 @@
|
||||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||||
import { database } from '../_database/database'
|
import {
|
||||||
|
getCustomEmoji as getCustomEmojiFromDatabase,
|
||||||
|
setCustomEmoji as setCustomEmojiInDatabase
|
||||||
|
} from '../_database/meta'
|
||||||
import { getCustomEmoji } from '../_api/emoji'
|
import { getCustomEmoji } from '../_api/emoji'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
|
|
||||||
export async function updateCustomEmojiForInstance (instanceName) {
|
export async function updateCustomEmojiForInstance (instanceName) {
|
||||||
await cacheFirstUpdateAfter(
|
await cacheFirstUpdateAfter(
|
||||||
() => getCustomEmoji(instanceName),
|
() => getCustomEmoji(instanceName),
|
||||||
() => database.getCustomEmoji(instanceName),
|
() => getCustomEmojiFromDatabase(instanceName),
|
||||||
emoji => database.setCustomEmoji(instanceName, emoji),
|
emoji => setCustomEmojiInDatabase(instanceName, emoji),
|
||||||
emoji => {
|
emoji => {
|
||||||
let { customEmoji } = store.get()
|
let { customEmoji } = store.get()
|
||||||
customEmoji[instanceName] = emoji
|
customEmoji[instanceName] = emoji
|
||||||
store.set({ customEmoji: customEmoji })
|
store.set({customEmoji: customEmoji})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function insertEmoji (realm, emoji) {
|
export function insertEmoji (realm, emoji) {
|
||||||
let emojiText = emoji.custom ? emoji.colons : emoji.native
|
|
||||||
let { composeSelectionStart } = store.get()
|
let { composeSelectionStart } = store.get()
|
||||||
let idx = composeSelectionStart || 0
|
let idx = composeSelectionStart || 0
|
||||||
let oldText = store.getComposeData(realm, 'text') || ''
|
let oldText = store.getComposeData(realm, 'text') || ''
|
||||||
let pre = oldText.substring(0, idx)
|
let pre = oldText.substring(0, idx)
|
||||||
let post = oldText.substring(idx)
|
let post = oldText.substring(idx)
|
||||||
let newText = `${pre}${emojiText} ${post}`
|
let newText = `${pre}:${emoji.shortcode}: ${post}`
|
||||||
store.setComposeData(realm, { text: newText })
|
store.setComposeData(realm, {text: newText})
|
||||||
}
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
import { favoriteStatus, unfavoriteStatus } from '../_api/favorite'
|
import { favoriteStatus, unfavoriteStatus } from '../_api/favorite'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { toast } from '../_components/toast/toast'
|
import { toast } from '../_utils/toast'
|
||||||
import { database } from '../_database/database'
|
import {
|
||||||
|
setStatusFavorited as setStatusFavoritedInDatabase
|
||||||
|
} from '../_database/timelines/updateStatus'
|
||||||
|
|
||||||
export async function setFavorited (statusId, favorited) {
|
export async function setFavorited (statusId, favorited) {
|
||||||
let { online } = store.get()
|
let { online } = store.get()
|
||||||
|
@ -16,7 +18,7 @@ export async function setFavorited (statusId, favorited) {
|
||||||
store.setStatusFavorited(currentInstance, statusId, favorited) // optimistic update
|
store.setStatusFavorited(currentInstance, statusId, favorited) // optimistic update
|
||||||
try {
|
try {
|
||||||
await networkPromise
|
await networkPromise
|
||||||
await database.setStatusFavorited(currentInstance, statusId, favorited)
|
await setStatusFavoritedInDatabase(currentInstance, statusId, favorited)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast.say(`Failed to ${favorited ? 'favorite' : 'unfavorite'}. ` + (e.message || ''))
|
toast.say(`Failed to ${favorited ? 'favorite' : 'unfavorite'}. ` + (e.message || ''))
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { store } from '../_store/store'
|
||||||
|
import { followAccount, unfollowAccount } from '../_api/follow'
|
||||||
|
import { toast } from '../_utils/toast'
|
||||||
|
import { updateProfileAndRelationship } from './accounts'
|
||||||
|
import {
|
||||||
|
getRelationship as getRelationshipFromDatabase
|
||||||
|
} from '../_database/accountsAndRelationships'
|
||||||
|
|
||||||
|
export async function setAccountFollowed (accountId, follow, toastOnSuccess) {
|
||||||
|
let { currentInstance, accessToken } = store.get()
|
||||||
|
try {
|
||||||
|
let account
|
||||||
|
if (follow) {
|
||||||
|
account = await followAccount(currentInstance, accessToken, accountId)
|
||||||
|
} else {
|
||||||
|
account = await unfollowAccount(currentInstance, accessToken, accountId)
|
||||||
|
}
|
||||||
|
await updateProfileAndRelationship(accountId)
|
||||||
|
let relationship = await getRelationshipFromDatabase(currentInstance, accountId)
|
||||||
|
if (toastOnSuccess) {
|
||||||
|
if (follow) {
|
||||||
|
if (account.locked && relationship.requested) {
|
||||||
|
toast.say('Requested to follow account')
|
||||||
|
} else {
|
||||||
|
toast.say('Followed account')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.say('Unfollowed account')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.say(`Unable to ${follow ? 'follow' : 'unfollow'} account: ` + (e.message || ''))
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,15 +3,15 @@ import { auth, basename } from '../_api/utils'
|
||||||
|
|
||||||
export async function getFollowRequests (instanceName, accessToken) {
|
export async function getFollowRequests (instanceName, accessToken) {
|
||||||
let url = `${basename(instanceName)}/api/v1/follow_requests`
|
let url = `${basename(instanceName)}/api/v1/follow_requests`
|
||||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function authorizeFollowRequest (instanceName, accessToken, id) {
|
export async function authorizeFollowRequest (instanceName, accessToken, id) {
|
||||||
let url = `${basename(instanceName)}/api/v1/follow_requests/${id}/authorize`
|
let url = `${basename(instanceName)}/api/v1/follow_requests/${id}/authorize`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rejectFollowRequest (instanceName, accessToken, id) {
|
export async function rejectFollowRequest (instanceName, accessToken, id) {
|
||||||
let url = `${basename(instanceName)}/api/v1/follow_requests/${id}/reject`
|
let url = `${basename(instanceName)}/api/v1/follow_requests/${id}/reject`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
|
@ -1,16 +1,22 @@
|
||||||
import { getVerifyCredentials } from '../_api/user'
|
import { getVerifyCredentials } from '../_api/user'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine'
|
import { switchToTheme } from '../_utils/themeEngine'
|
||||||
import { toast } from '../_components/toast/toast'
|
import { toast } from '../_utils/toast'
|
||||||
import { goto } from '../../../__sapper__/client'
|
import { goto } from 'sapper/runtime.js'
|
||||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||||
import { getInstanceInfo } from '../_api/instance'
|
import { getInstanceInfo } from '../_api/instance'
|
||||||
import { database } from '../_database/database'
|
import { clearDatabaseForInstance } from '../_database/clear'
|
||||||
|
import {
|
||||||
|
getInstanceVerifyCredentials as getInstanceVerifyCredentialsFromDatabase,
|
||||||
|
setInstanceVerifyCredentials as setInstanceVerifyCredentialsInDatabase,
|
||||||
|
getInstanceInfo as getInstanceInfoFromDatabase,
|
||||||
|
setInstanceInfo as setInstanceInfoInDatabase
|
||||||
|
} from '../_database/meta'
|
||||||
|
|
||||||
export function changeTheme (instanceName, newTheme) {
|
export function changeTheme (instanceName, newTheme) {
|
||||||
let { instanceThemes } = store.get()
|
let { instanceThemes } = store.get()
|
||||||
instanceThemes[instanceName] = newTheme
|
instanceThemes[instanceName] = newTheme
|
||||||
store.set({ instanceThemes: instanceThemes })
|
store.set({instanceThemes: instanceThemes})
|
||||||
store.save()
|
store.save()
|
||||||
let { currentInstance } = store.get()
|
let { currentInstance } = store.get()
|
||||||
if (instanceName === currentInstance) {
|
if (instanceName === currentInstance) {
|
||||||
|
@ -55,15 +61,15 @@ export async function logOutOfInstance (instanceName) {
|
||||||
})
|
})
|
||||||
store.save()
|
store.save()
|
||||||
toast.say(`Logged out of ${instanceName}`)
|
toast.say(`Logged out of ${instanceName}`)
|
||||||
switchToTheme(instanceThemes[newInstance] || DEFAULT_THEME)
|
switchToTheme(instanceThemes[newInstance] || 'default')
|
||||||
/* no await */ database.clearDatabaseForInstance(instanceName)
|
await clearDatabaseForInstance(instanceName)
|
||||||
goto('/settings/instances')
|
goto('/settings/instances')
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStoreVerifyCredentials (instanceName, thisVerifyCredentials) {
|
function setStoreVerifyCredentials (instanceName, thisVerifyCredentials) {
|
||||||
let { verifyCredentials } = store.get()
|
let { verifyCredentials } = store.get()
|
||||||
verifyCredentials[instanceName] = thisVerifyCredentials
|
verifyCredentials[instanceName] = thisVerifyCredentials
|
||||||
store.set({ verifyCredentials: verifyCredentials })
|
store.set({verifyCredentials: verifyCredentials})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateVerifyCredentialsForInstance (instanceName) {
|
export async function updateVerifyCredentialsForInstance (instanceName) {
|
||||||
|
@ -71,8 +77,8 @@ export async function updateVerifyCredentialsForInstance (instanceName) {
|
||||||
let accessToken = loggedInInstances[instanceName].access_token
|
let accessToken = loggedInInstances[instanceName].access_token
|
||||||
await cacheFirstUpdateAfter(
|
await cacheFirstUpdateAfter(
|
||||||
() => getVerifyCredentials(instanceName, accessToken),
|
() => getVerifyCredentials(instanceName, accessToken),
|
||||||
() => database.getInstanceVerifyCredentials(instanceName),
|
() => getInstanceVerifyCredentialsFromDatabase(instanceName),
|
||||||
verifyCredentials => database.setInstanceVerifyCredentials(instanceName, verifyCredentials),
|
verifyCredentials => setInstanceVerifyCredentialsInDatabase(instanceName, verifyCredentials),
|
||||||
verifyCredentials => setStoreVerifyCredentials(instanceName, verifyCredentials)
|
verifyCredentials => setStoreVerifyCredentials(instanceName, verifyCredentials)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -85,12 +91,12 @@ export async function updateVerifyCredentialsForCurrentInstance () {
|
||||||
export async function updateInstanceInfo (instanceName) {
|
export async function updateInstanceInfo (instanceName) {
|
||||||
await cacheFirstUpdateAfter(
|
await cacheFirstUpdateAfter(
|
||||||
() => getInstanceInfo(instanceName),
|
() => getInstanceInfo(instanceName),
|
||||||
() => database.getInstanceInfo(instanceName),
|
() => getInstanceInfoFromDatabase(instanceName),
|
||||||
info => database.setInstanceInfo(instanceName, info),
|
info => setInstanceInfoInDatabase(instanceName, info),
|
||||||
info => {
|
info => {
|
||||||
let { instanceInfos } = store.get()
|
let { instanceInfos } = store.get()
|
||||||
instanceInfos[instanceName] = info
|
instanceInfos[instanceName] = info
|
||||||
store.set({ instanceInfos: instanceInfos })
|
store.set({instanceInfos: instanceInfos})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { store } from '../_store/store'
|
||||||
|
import { getLists } from '../_api/lists'
|
||||||
|
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||||
|
import {
|
||||||
|
getLists as getListsFromDatabase,
|
||||||
|
setLists as setListsInDatabase
|
||||||
|
} from '../_database/meta'
|
||||||
|
|
||||||
|
export async function updateLists () {
|
||||||
|
let { currentInstance, accessToken } = store.get()
|
||||||
|
|
||||||
|
await cacheFirstUpdateAfter(
|
||||||
|
() => getLists(currentInstance, accessToken),
|
||||||
|
() => getListsFromDatabase(currentInstance),
|
||||||
|
lists => setListsInDatabase(currentInstance, lists),
|
||||||
|
lists => {
|
||||||
|
let { instanceLists } = store.get()
|
||||||
|
instanceLists[currentInstance] = lists
|
||||||
|
store.set({instanceLists: instanceLists})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,40 +1,49 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { uploadMedia } from '../_api/media'
|
import { uploadMedia } from '../_api/media'
|
||||||
import { toast } from '../_components/toast/toast'
|
import { toast } from '../_utils/toast'
|
||||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||||
|
|
||||||
export async function doMediaUpload (realm, file) {
|
export async function doMediaUpload (realm, file) {
|
||||||
let { currentInstance, accessToken } = store.get()
|
let { currentInstance, accessToken } = store.get()
|
||||||
store.set({ uploadingMedia: true })
|
store.set({uploadingMedia: true})
|
||||||
try {
|
try {
|
||||||
let response = await uploadMedia(currentInstance, accessToken, file)
|
let response = await uploadMedia(currentInstance, accessToken, file)
|
||||||
let composeMedia = store.getComposeData(realm, 'media') || []
|
let composeMedia = store.getComposeData(realm, 'media') || []
|
||||||
if (composeMedia.length === 4) {
|
|
||||||
throw new Error('Only 4 media max are allowed')
|
|
||||||
}
|
|
||||||
composeMedia.push({
|
composeMedia.push({
|
||||||
data: response,
|
data: response,
|
||||||
file: { name: file.name },
|
file: { name: file.name }
|
||||||
description: ''
|
|
||||||
})
|
})
|
||||||
|
let composeText = store.getComposeData(realm, 'text') || ''
|
||||||
|
composeText += ' ' + response.text_url
|
||||||
store.setComposeData(realm, {
|
store.setComposeData(realm, {
|
||||||
media: composeMedia
|
media: composeMedia,
|
||||||
|
text: composeText
|
||||||
})
|
})
|
||||||
scheduleIdleTask(() => store.save())
|
scheduleIdleTask(() => store.save())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast.say('Failed to upload media: ' + (e.message || ''))
|
toast.say('Failed to upload media: ' + (e.message || ''))
|
||||||
} finally {
|
} finally {
|
||||||
store.set({ uploadingMedia: false })
|
store.set({uploadingMedia: false})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteMedia (realm, i) {
|
export function deleteMedia (realm, i) {
|
||||||
let composeMedia = store.getComposeData(realm, 'media')
|
let composeMedia = store.getComposeData(realm, 'media')
|
||||||
composeMedia.splice(i, 1)
|
let deletedMedia = composeMedia.splice(i, 1)[0]
|
||||||
|
|
||||||
|
let composeText = store.getComposeData(realm, 'text') || ''
|
||||||
|
composeText = composeText.replace(' ' + deletedMedia.data.text_url, '')
|
||||||
|
|
||||||
|
let mediaDescriptions = store.getComposeData(realm, 'mediaDescriptions') || []
|
||||||
|
if (mediaDescriptions[i]) {
|
||||||
|
mediaDescriptions[i] = null
|
||||||
|
}
|
||||||
|
|
||||||
store.setComposeData(realm, {
|
store.setComposeData(realm, {
|
||||||
media: composeMedia
|
media: composeMedia,
|
||||||
|
text: composeText,
|
||||||
|
mediaDescriptions: mediaDescriptions
|
||||||
})
|
})
|
||||||
scheduleIdleTask(() => store.save())
|
scheduleIdleTask(() => store.save())
|
||||||
}
|
}
|
|
@ -1,19 +1,18 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { muteAccount, unmuteAccount } from '../_api/mute'
|
import { muteAccount, unmuteAccount } from '../_api/mute'
|
||||||
import { toast } from '../_components/toast/toast'
|
import { toast } from '../_utils/toast'
|
||||||
import { updateLocalRelationship } from './accounts'
|
import { updateProfileAndRelationship } from './accounts'
|
||||||
import { emit } from '../_utils/eventBus'
|
import { emit } from '../_utils/eventBus'
|
||||||
|
|
||||||
export async function setAccountMuted (accountId, mute, toastOnSuccess) {
|
export async function setAccountMuted (accountId, mute, toastOnSuccess) {
|
||||||
let { currentInstance, accessToken } = store.get()
|
let { currentInstance, accessToken } = store.get()
|
||||||
try {
|
try {
|
||||||
let relationship
|
|
||||||
if (mute) {
|
if (mute) {
|
||||||
relationship = await muteAccount(currentInstance, accessToken, accountId)
|
await muteAccount(currentInstance, accessToken, accountId)
|
||||||
} else {
|
} else {
|
||||||
relationship = await unmuteAccount(currentInstance, accessToken, accountId)
|
await unmuteAccount(currentInstance, accessToken, accountId)
|
||||||
}
|
}
|
||||||
await updateLocalRelationship(currentInstance, accountId, relationship)
|
await updateProfileAndRelationship(accountId)
|
||||||
if (toastOnSuccess) {
|
if (toastOnSuccess) {
|
||||||
if (mute) {
|
if (mute) {
|
||||||
toast.say('Muted account')
|
toast.say('Muted account')
|
|
@ -1,7 +1,7 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { muteConversation, unmuteConversation } from '../_api/muteConversation'
|
import { muteConversation, unmuteConversation } from '../_api/muteConversation'
|
||||||
import { toast } from '../_components/toast/toast'
|
import { toast } from '../_utils/toast'
|
||||||
import { database } from '../_database/database'
|
import { setStatusMuted as setStatusMutedInDatabase } from '../_database/timelines/updateStatus'
|
||||||
|
|
||||||
export async function setConversationMuted (statusId, mute, toastOnSuccess) {
|
export async function setConversationMuted (statusId, mute, toastOnSuccess) {
|
||||||
let { currentInstance, accessToken } = store.get()
|
let { currentInstance, accessToken } = store.get()
|
||||||
|
@ -11,7 +11,7 @@ export async function setConversationMuted (statusId, mute, toastOnSuccess) {
|
||||||
} else {
|
} else {
|
||||||
await unmuteConversation(currentInstance, accessToken, statusId)
|
await unmuteConversation(currentInstance, accessToken, statusId)
|
||||||
}
|
}
|
||||||
await database.setStatusMuted(currentInstance, statusId, mute)
|
await setStatusMutedInDatabase(currentInstance, statusId, mute)
|
||||||
if (toastOnSuccess) {
|
if (toastOnSuccess) {
|
||||||
if (mute) {
|
if (mute) {
|
||||||
toast.say('Muted conversation')
|
toast.say('Muted conversation')
|
|
@ -1,7 +1,7 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { toast } from '../_components/toast/toast'
|
import { toast } from '../_utils/toast'
|
||||||
import { pinStatus, unpinStatus } from '../_api/pin'
|
import { pinStatus, unpinStatus } from '../_api/pin'
|
||||||
import { database } from '../_database/database'
|
import { setStatusPinned as setStatusPinnedInDatabase } from '../_database/timelines/updateStatus'
|
||||||
import { emit } from '../_utils/eventBus'
|
import { emit } from '../_utils/eventBus'
|
||||||
|
|
||||||
export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSuccess) {
|
export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSuccess) {
|
||||||
|
@ -19,8 +19,7 @@ export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSucces
|
||||||
toast.say('Unpinned status')
|
toast.say('Unpinned status')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
store.setStatusPinned(currentInstance, statusId, pinned)
|
await setStatusPinnedInDatabase(currentInstance, statusId, pinned)
|
||||||
await database.setStatusPinned(currentInstance, statusId, pinned)
|
|
||||||
emit('updatePinnedStatuses')
|
emit('updatePinnedStatuses')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
|
@ -1,6 +1,9 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||||
import { database } from '../_database/database'
|
import {
|
||||||
|
getPinnedStatuses as getPinnedStatusesFromDatabase,
|
||||||
|
insertPinnedStatuses as insertPinnedStatusesInDatabase
|
||||||
|
} from '../_database/timelines/pinnedStatuses'
|
||||||
import {
|
import {
|
||||||
getPinnedStatuses
|
getPinnedStatuses
|
||||||
} from '../_api/pinnedStatuses'
|
} from '../_api/pinnedStatuses'
|
||||||
|
@ -10,13 +13,13 @@ export async function updatePinnedStatusesForAccount (accountId) {
|
||||||
|
|
||||||
await cacheFirstUpdateAfter(
|
await cacheFirstUpdateAfter(
|
||||||
() => getPinnedStatuses(currentInstance, accessToken, accountId),
|
() => getPinnedStatuses(currentInstance, accessToken, accountId),
|
||||||
() => database.getPinnedStatuses(currentInstance, accountId),
|
() => getPinnedStatusesFromDatabase(currentInstance, accountId),
|
||||||
statuses => database.insertPinnedStatuses(currentInstance, accountId, statuses),
|
statuses => insertPinnedStatusesInDatabase(currentInstance, accountId, statuses),
|
||||||
statuses => {
|
statuses => {
|
||||||
let { pinnedStatuses } = store.get()
|
let { pinnedStatuses } = store.get()
|
||||||
pinnedStatuses[currentInstance] = pinnedStatuses[currentInstance] || {}
|
pinnedStatuses[currentInstance] = pinnedStatuses[currentInstance] || {}
|
||||||
pinnedStatuses[currentInstance][accountId] = statuses
|
pinnedStatuses[currentInstance][accountId] = statuses
|
||||||
store.set({ pinnedStatuses: pinnedStatuses })
|
store.set({pinnedStatuses: pinnedStatuses})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -2,5 +2,5 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
|
|
||||||
export function setPostPrivacy (realm, postPrivacyKey) {
|
export function setPostPrivacy (realm, postPrivacyKey) {
|
||||||
store.setComposeData(realm, { postPrivacy: postPrivacyKey })
|
store.setComposeData(realm, {postPrivacy: postPrivacyKey})
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { toast } from '../_components/toast/toast'
|
import { toast } from '../_utils/toast'
|
||||||
import { reblogStatus, unreblogStatus } from '../_api/reblog'
|
import { reblogStatus, unreblogStatus } from '../_api/reblog'
|
||||||
import { database } from '../_database/database'
|
import { setStatusReblogged as setStatusRebloggedInDatabase } from '../_database/timelines/updateStatus'
|
||||||
|
|
||||||
export async function setReblogged (statusId, reblogged) {
|
export async function setReblogged (statusId, reblogged) {
|
||||||
let online = store.get()
|
let online = store.get()
|
||||||
|
@ -16,7 +16,7 @@ export async function setReblogged (statusId, reblogged) {
|
||||||
store.setStatusReblogged(currentInstance, statusId, reblogged) // optimistic update
|
store.setStatusReblogged(currentInstance, statusId, reblogged) // optimistic update
|
||||||
try {
|
try {
|
||||||
await networkPromise
|
await networkPromise
|
||||||
await database.setStatusReblogged(currentInstance, statusId, reblogged)
|
await setStatusRebloggedInDatabase(currentInstance, statusId, reblogged)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast.say(`Failed to ${reblogged ? 'boost' : 'unboost'}. ` + (e.message || ''))
|
toast.say(`Failed to ${reblogged ? 'boost' : 'unboost'}. ` + (e.message || ''))
|
|
@ -1,7 +1,7 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { approveFollowRequest, rejectFollowRequest } from '../_api/requests'
|
import { approveFollowRequest, rejectFollowRequest } from '../_api/requests'
|
||||||
import { emit } from '../_utils/eventBus'
|
import { emit } from '../_utils/eventBus'
|
||||||
import { toast } from '../_components/toast/toast'
|
import { toast } from '../_utils/toast'
|
||||||
|
|
||||||
export async function setFollowRequestApprovedOrRejected (accountId, approved, toastOnSuccess) {
|
export async function setFollowRequestApprovedOrRejected (accountId, approved, toastOnSuccess) {
|
||||||
let {
|
let {
|
|
@ -1,10 +1,10 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { toast } from '../_components/toast/toast'
|
import { toast } from '../_utils/toast'
|
||||||
import { search } from '../_api/search'
|
import { search } from '../_api/search'
|
||||||
|
|
||||||
export async function doSearch () {
|
export async function doSearch () {
|
||||||
let { currentInstance, accessToken, queryInSearch } = store.get()
|
let { currentInstance, accessToken, queryInSearch } = store.get()
|
||||||
store.set({ searchLoading: true })
|
store.set({searchLoading: true})
|
||||||
try {
|
try {
|
||||||
let results = await search(currentInstance, accessToken, queryInSearch)
|
let results = await search(currentInstance, accessToken, queryInSearch)
|
||||||
let { queryInSearch: newQueryInSearch } = store.get() // avoid race conditions
|
let { queryInSearch: newQueryInSearch } = store.get() // avoid race conditions
|
||||||
|
@ -18,6 +18,6 @@ export async function doSearch () {
|
||||||
toast.say('Error during search: ' + (e.name || '') + ' ' + (e.message || ''))
|
toast.say('Error during search: ' + (e.name || '') + ' ' + (e.message || ''))
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
store.set({ searchLoading: false })
|
store.set({searchLoading: false})
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,13 @@
|
||||||
import { database } from '../_database/database'
|
import {
|
||||||
|
getNotificationIdsForStatuses as getNotificationIdsForStatusesFromDatabase,
|
||||||
|
getReblogsForStatus as getReblogsForStatusFromDatabase
|
||||||
|
} from '../_database/timelines/lookup'
|
||||||
|
import {
|
||||||
|
getStatus as getStatusFromDatabase
|
||||||
|
} from '../_database/timelines/getStatusOrNotification'
|
||||||
|
|
||||||
export async function getIdThatThisStatusReblogged (instanceName, statusId) {
|
export async function getIdThatThisStatusReblogged (instanceName, statusId) {
|
||||||
let status = await database.getStatus(instanceName, statusId)
|
let status = await getStatusFromDatabase(instanceName, statusId)
|
||||||
return status.reblog && status.reblog.id
|
return status.reblog && status.reblog.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,9 +19,9 @@ export async function getIdsThatTheseStatusesReblogged (instanceName, statusIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getIdsThatRebloggedThisStatus (instanceName, statusId) {
|
export async function getIdsThatRebloggedThisStatus (instanceName, statusId) {
|
||||||
return database.getReblogsForStatus(instanceName, statusId)
|
return getReblogsForStatusFromDatabase(instanceName, statusId)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNotificationIdsForStatuses (instanceName, statusIds) {
|
export async function getNotificationIdsForStatuses (instanceName, statusIds) {
|
||||||
return database.getNotificationIdsForStatuses(instanceName, statusIds)
|
return getNotificationIdsForStatusesFromDatabase(instanceName, statusIds)
|
||||||
}
|
}
|
|
@ -1,55 +1,34 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { getTimeline } from '../_api/timelines'
|
import { getTimeline } from '../_api/timelines'
|
||||||
import { toast } from '../_components/toast/toast'
|
import { toast } from '../_utils/toast'
|
||||||
import { mark, stop } from '../_utils/marks'
|
import { mark, stop } from '../_utils/marks'
|
||||||
import { concat, mergeArrays } from '../_utils/arrays'
|
import { mergeArrays } from '../_utils/arrays'
|
||||||
import { byItemIds } from '../_utils/sorting'
|
import { byItemIds } from '../_utils/sorting'
|
||||||
import isEqual from 'lodash-es/isEqual'
|
import isEqual from 'lodash-es/isEqual'
|
||||||
import { database } from '../_database/database'
|
import {
|
||||||
import { getStatus, getStatusContext } from '../_api/statuses'
|
insertTimelineItems as insertTimelineItemsInDatabase
|
||||||
import { emit } from '../_utils/eventBus'
|
} from '../_database/timelines/insertion'
|
||||||
import { TIMELINE_BATCH_SIZE } from '../_static/timelines'
|
import {
|
||||||
|
getTimeline as getTimelineFromDatabase
|
||||||
|
} from '../_database/timelines/pagination'
|
||||||
|
|
||||||
async function storeFreshTimelineItemsInDatabase (instanceName, timelineName, items) {
|
const FETCH_LIMIT = 20
|
||||||
await database.insertTimelineItems(instanceName, timelineName, items)
|
|
||||||
if (timelineName.startsWith('status/')) {
|
|
||||||
// For status threads, we want to be sure to update the favorite/reblog counts even if
|
|
||||||
// this is a stale "view" of the status. See 119-status-counts-update.js for
|
|
||||||
// an example of why we need this.
|
|
||||||
items.forEach(item => {
|
|
||||||
emit('statusUpdated', item)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchTimelineItemsFromNetwork (instanceName, accessToken, timelineName, lastTimelineItemId) {
|
|
||||||
if (timelineName.startsWith('status/')) { // special case - this is a list of descendents and ancestors
|
|
||||||
let statusId = timelineName.split('/').slice(-1)[0]
|
|
||||||
let statusRequest = getStatus(instanceName, accessToken, statusId)
|
|
||||||
let contextRequest = getStatusContext(instanceName, accessToken, statusId)
|
|
||||||
let [ status, context ] = await Promise.all([statusRequest, contextRequest])
|
|
||||||
return concat(context.ancestors, status, context.descendants)
|
|
||||||
} else { // normal timeline
|
|
||||||
return getTimeline(instanceName, accessToken, timelineName, lastTimelineItemId, null, TIMELINE_BATCH_SIZE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchTimelineItems (instanceName, accessToken, timelineName, lastTimelineItemId, online) {
|
async function fetchTimelineItems (instanceName, accessToken, timelineName, lastTimelineItemId, online) {
|
||||||
mark('fetchTimelineItems')
|
mark('fetchTimelineItems')
|
||||||
let items
|
let items
|
||||||
let stale = false
|
let stale = false
|
||||||
if (!online) {
|
if (!online) {
|
||||||
items = await database.getTimeline(instanceName, timelineName, lastTimelineItemId, TIMELINE_BATCH_SIZE)
|
items = await getTimelineFromDatabase(instanceName, timelineName, lastTimelineItemId, FETCH_LIMIT)
|
||||||
stale = true
|
stale = true
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
console.log('fetchTimelineItemsFromNetwork')
|
items = await getTimeline(instanceName, accessToken, timelineName, lastTimelineItemId, FETCH_LIMIT)
|
||||||
items = await fetchTimelineItemsFromNetwork(instanceName, accessToken, timelineName, lastTimelineItemId)
|
/* no await */ insertTimelineItemsInDatabase(instanceName, timelineName, items)
|
||||||
/* no await */ storeFreshTimelineItemsInDatabase(instanceName, timelineName, items)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast.say('Internet request failed. Showing offline content.')
|
toast.say('Internet request failed. Showing offline content.')
|
||||||
items = await database.getTimeline(instanceName, timelineName, lastTimelineItemId, TIMELINE_BATCH_SIZE)
|
items = await getTimelineFromDatabase(instanceName, timelineName, lastTimelineItemId, FETCH_LIMIT)
|
||||||
stale = true
|
stale = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,10 +51,10 @@ export async function addTimelineItemIds (instanceName, timelineName, newIds, ne
|
||||||
let mergedIds = mergeArrays(oldIds || [], newIds)
|
let mergedIds = mergeArrays(oldIds || [], newIds)
|
||||||
|
|
||||||
if (!isEqual(oldIds, mergedIds)) {
|
if (!isEqual(oldIds, mergedIds)) {
|
||||||
store.setForTimeline(instanceName, timelineName, { timelineItemIds: mergedIds })
|
store.setForTimeline(instanceName, timelineName, {timelineItemIds: mergedIds})
|
||||||
}
|
}
|
||||||
if (oldStale !== newStale) {
|
if (oldStale !== newStale) {
|
||||||
store.setForTimeline(instanceName, timelineName, { timelineItemIdsAreStale: newStale })
|
store.setForTimeline(instanceName, timelineName, {timelineItemIdsAreStale: newStale})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,10 +93,8 @@ export async function setupTimeline () {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTimelineItemsOnScrollToBottom (instanceName, timelineName) {
|
export async function fetchTimelineItemsOnScrollToBottom (instanceName, timelineName) {
|
||||||
console.log('setting runningUpdate: true')
|
|
||||||
store.setForTimeline(instanceName, timelineName, { runningUpdate: true })
|
store.setForTimeline(instanceName, timelineName, { runningUpdate: true })
|
||||||
await fetchTimelineItemsAndPossiblyFallBack()
|
await fetchTimelineItemsAndPossiblyFallBack()
|
||||||
console.log('setting runningUpdate: false')
|
|
||||||
store.setForTimeline(instanceName, timelineName, { runningUpdate: false })
|
store.setForTimeline(instanceName, timelineName, { runningUpdate: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,11 +124,7 @@ export async function showMoreItemsForThread (instanceName, timelineName) {
|
||||||
let itemIdsToAdd = store.getForTimeline(instanceName, timelineName, 'itemIdsToAdd')
|
let itemIdsToAdd = store.getForTimeline(instanceName, timelineName, 'itemIdsToAdd')
|
||||||
let timelineItemIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds')
|
let timelineItemIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds')
|
||||||
// TODO: update database and do the thread merge correctly
|
// TODO: update database and do the thread merge correctly
|
||||||
for (let itemIdToAdd of itemIdsToAdd) {
|
timelineItemIds = timelineItemIds.concat(itemIdsToAdd)
|
||||||
if (!timelineItemIds.includes(itemIdToAdd)) {
|
|
||||||
timelineItemIds.push(itemIdToAdd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
store.setForTimeline(instanceName, timelineName, {
|
store.setForTimeline(instanceName, timelineName, {
|
||||||
itemIdsToAdd: [],
|
itemIdsToAdd: [],
|
||||||
timelineItemIds: timelineItemIds
|
timelineItemIds: timelineItemIds
|
|
@ -3,10 +3,10 @@ import { post, WRITE_TIMEOUT } from '../_utils/ajax'
|
||||||
|
|
||||||
export async function blockAccount (instanceName, accessToken, accountId) {
|
export async function blockAccount (instanceName, accessToken, accountId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/block`
|
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/block`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unblockAccount (instanceName, accessToken, accountId) {
|
export async function unblockAccount (instanceName, accessToken, accountId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/unblock`
|
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/unblock`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
|
@ -4,11 +4,11 @@ import { auth, basename } from './utils'
|
||||||
export async function getBlockedAccounts (instanceName, accessToken, limit = 80) {
|
export async function getBlockedAccounts (instanceName, accessToken, limit = 80) {
|
||||||
let url = `${basename(instanceName)}/api/v1/blocks`
|
let url = `${basename(instanceName)}/api/v1/blocks`
|
||||||
url += '?' + paramsString({ limit })
|
url += '?' + paramsString({ limit })
|
||||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMutedAccounts (instanceName, accessToken, limit = 80) {
|
export async function getMutedAccounts (instanceName, accessToken, limit = 80) {
|
||||||
let url = `${basename(instanceName)}/api/v1/mutes`
|
let url = `${basename(instanceName)}/api/v1/mutes`
|
||||||
url += '?' + paramsString({ limit })
|
url += '?' + paramsString({ limit })
|
||||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||||
}
|
}
|
|
@ -3,5 +3,5 @@ import { del, WRITE_TIMEOUT } from '../_utils/ajax'
|
||||||
|
|
||||||
export async function deleteStatus (instanceName, accessToken, statusId) {
|
export async function deleteStatus (instanceName, accessToken, statusId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}`
|
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}`
|
||||||
return del(url, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return del(url, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
|
@ -3,5 +3,5 @@ import { DEFAULT_TIMEOUT, get } from '../_utils/ajax'
|
||||||
|
|
||||||
export async function getCustomEmoji (instanceName) {
|
export async function getCustomEmoji (instanceName) {
|
||||||
let url = `${basename(instanceName)}/api/v1/custom_emojis`
|
let url = `${basename(instanceName)}/api/v1/custom_emojis`
|
||||||
return get(url, null, { timeout: DEFAULT_TIMEOUT })
|
return get(url, null, {timeout: DEFAULT_TIMEOUT})
|
||||||
}
|
}
|
|
@ -3,10 +3,10 @@ import { basename, auth } from './utils'
|
||||||
|
|
||||||
export async function favoriteStatus (instanceName, accessToken, statusId) {
|
export async function favoriteStatus (instanceName, accessToken, statusId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/favourite`
|
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/favourite`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unfavoriteStatus (instanceName, accessToken, statusId) {
|
export async function unfavoriteStatus (instanceName, accessToken, statusId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unfavourite`
|
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unfavourite`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
|
@ -3,10 +3,10 @@ import { auth, basename } from './utils'
|
||||||
|
|
||||||
export async function followAccount (instanceName, accessToken, accountId) {
|
export async function followAccount (instanceName, accessToken, accountId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/follow`
|
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/follow`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unfollowAccount (instanceName, accessToken, accountId) {
|
export async function unfollowAccount (instanceName, accessToken, accountId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/unfollow`
|
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/unfollow`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
|
@ -4,11 +4,11 @@ import { auth, basename } from './utils'
|
||||||
export async function getFollows (instanceName, accessToken, accountId, limit = 80) {
|
export async function getFollows (instanceName, accessToken, accountId, limit = 80) {
|
||||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/following`
|
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/following`
|
||||||
url += '?' + paramsString({ limit })
|
url += '?' + paramsString({ limit })
|
||||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFollowers (instanceName, accessToken, accountId, limit = 80) {
|
export async function getFollowers (instanceName, accessToken, accountId, limit = 80) {
|
||||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/followers`
|
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/followers`
|
||||||
url += '?' + paramsString({ limit })
|
url += '?' + paramsString({ limit })
|
||||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||||
}
|
}
|
|
@ -3,5 +3,5 @@ import { basename } from './utils'
|
||||||
|
|
||||||
export function getInstanceInfo (instanceName) {
|
export function getInstanceInfo (instanceName) {
|
||||||
let url = `${basename(instanceName)}/api/v1/instance`
|
let url = `${basename(instanceName)}/api/v1/instance`
|
||||||
return get(url, null, { timeout: DEFAULT_TIMEOUT })
|
return get(url, null, {timeout: DEFAULT_TIMEOUT})
|
||||||
}
|
}
|
|
@ -3,5 +3,5 @@ import { auth, basename } from './utils'
|
||||||
|
|
||||||
export function getLists (instanceName, accessToken) {
|
export function getLists (instanceName, accessToken) {
|
||||||
let url = `${basename(instanceName)}/api/v1/lists`
|
let url = `${basename(instanceName)}/api/v1/lists`
|
||||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||||
}
|
}
|
|
@ -8,10 +8,10 @@ export async function uploadMedia (instanceName, accessToken, file, description)
|
||||||
formData.append('description', description)
|
formData.append('description', description)
|
||||||
}
|
}
|
||||||
let url = `${basename(instanceName)}/api/v1/media`
|
let url = `${basename(instanceName)}/api/v1/media`
|
||||||
return post(url, formData, auth(accessToken), { timeout: MEDIA_WRITE_TIMEOUT })
|
return post(url, formData, auth(accessToken), {timeout: MEDIA_WRITE_TIMEOUT})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putMediaDescription (instanceName, accessToken, mediaId, description) {
|
export async function putMediaDescription (instanceName, accessToken, mediaId, description) {
|
||||||
let url = `${basename(instanceName)}/api/v1/media/${mediaId}`
|
let url = `${basename(instanceName)}/api/v1/media/${mediaId}`
|
||||||
return put(url, { description }, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return put(url, {description}, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
|
@ -3,10 +3,10 @@ import { post, WRITE_TIMEOUT } from '../_utils/ajax'
|
||||||
|
|
||||||
export async function muteAccount (instanceName, accessToken, accountId) {
|
export async function muteAccount (instanceName, accessToken, accountId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/mute`
|
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/mute`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unmuteAccount (instanceName, accessToken, accountId) {
|
export async function unmuteAccount (instanceName, accessToken, accountId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/unmute`
|
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/unmute`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
|
@ -3,10 +3,10 @@ import { post, WRITE_TIMEOUT } from '../_utils/ajax'
|
||||||
|
|
||||||
export async function muteConversation (instanceName, accessToken, statusId) {
|
export async function muteConversation (instanceName, accessToken, statusId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/mute`
|
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/mute`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unmuteConversation (instanceName, accessToken, statusId) {
|
export async function unmuteConversation (instanceName, accessToken, statusId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unmute`
|
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unmute`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@ import { post, paramsString, WRITE_TIMEOUT } from '../_utils/ajax'
|
||||||
import { basename } from './utils'
|
import { basename } from './utils'
|
||||||
|
|
||||||
const WEBSITE = 'https://pinafore.social'
|
const WEBSITE = 'https://pinafore.social'
|
||||||
const SCOPES = 'read write follow push'
|
const SCOPES = 'read write follow'
|
||||||
const CLIENT_NAME = 'Pinafore'
|
const CLIENT_NAME = 'Pinafore'
|
||||||
|
|
||||||
export function registerApplication (instanceName, redirectUri) {
|
export function registerApplication (instanceName, redirectUri) {
|
||||||
|
@ -12,7 +12,7 @@ export function registerApplication (instanceName, redirectUri) {
|
||||||
redirect_uris: redirectUri,
|
redirect_uris: redirectUri,
|
||||||
scopes: SCOPES,
|
scopes: SCOPES,
|
||||||
website: WEBSITE
|
website: WEBSITE
|
||||||
}, null, { timeout: WRITE_TIMEOUT })
|
}, null, {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateAuthLink (instanceName, clientId, redirectUri) {
|
export function generateAuthLink (instanceName, clientId, redirectUri) {
|
||||||
|
@ -33,5 +33,5 @@ export function getAccessTokenFromAuthCode (instanceName, clientId, clientSecret
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
code: code
|
code: code
|
||||||
}, null, { timeout: WRITE_TIMEOUT })
|
}, null, {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
|
@ -3,10 +3,10 @@ import { auth, basename } from './utils'
|
||||||
|
|
||||||
export async function pinStatus (instanceName, accessToken, statusId) {
|
export async function pinStatus (instanceName, accessToken, statusId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/pin`
|
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/pin`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unpinStatus (instanceName, accessToken, statusId) {
|
export async function unpinStatus (instanceName, accessToken, statusId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unpin`
|
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unpin`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
|
@ -7,5 +7,5 @@ export async function getPinnedStatuses (instanceName, accessToken, accountId) {
|
||||||
limit: 40,
|
limit: 40,
|
||||||
pinned: true
|
pinned: true
|
||||||
})
|
})
|
||||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||||
}
|
}
|
|
@ -4,11 +4,11 @@ import { auth, basename } from './utils'
|
||||||
export async function getReblogs (instanceName, accessToken, statusId, limit = 80) {
|
export async function getReblogs (instanceName, accessToken, statusId, limit = 80) {
|
||||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/reblogged_by`
|
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/reblogged_by`
|
||||||
url += '?' + paramsString({ limit })
|
url += '?' + paramsString({ limit })
|
||||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFavorites (instanceName, accessToken, statusId, limit = 80) {
|
export async function getFavorites (instanceName, accessToken, statusId, limit = 80) {
|
||||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/favourited_by`
|
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/favourited_by`
|
||||||
url += '?' + paramsString({ limit })
|
url += '?' + paramsString({ limit })
|
||||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||||
}
|
}
|
|
@ -3,10 +3,10 @@ import { auth, basename } from './utils'
|
||||||
|
|
||||||
export async function approveFollowRequest (instanceName, accessToken, accountId) {
|
export async function approveFollowRequest (instanceName, accessToken, accountId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/follow_requests/${accountId}/authorize`
|
let url = `${basename(instanceName)}/api/v1/follow_requests/${accountId}/authorize`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rejectFollowRequest (instanceName, accessToken, accountId) {
|
export async function rejectFollowRequest (instanceName, accessToken, accountId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/follow_requests/${accountId}/reject`
|
let url = `${basename(instanceName)}/api/v1/follow_requests/${accountId}/reject`
|
||||||
return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return post(url, null, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
}
|
}
|
|
@ -6,5 +6,5 @@ export function search (instanceName, accessToken, query) {
|
||||||
q: query,
|
q: query,
|
||||||
resolve: true
|
resolve: true
|
||||||
})
|
})
|
||||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||||
}
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { auth, basename } from './utils'
|
||||||
|
import { post, WRITE_TIMEOUT } from '../_utils/ajax'
|
||||||
|
|
||||||
|
export async function postStatus (instanceName, accessToken, text, inReplyToId, mediaIds,
|
||||||
|
sensitive, spoilerText, visibility) {
|
||||||
|
let url = `${basename(instanceName)}/api/v1/statuses`
|
||||||
|
|
||||||
|
let body = {
|
||||||
|
status: text,
|
||||||
|
in_reply_to_id: inReplyToId,
|
||||||
|
media_ids: mediaIds,
|
||||||
|
sensitive: sensitive,
|
||||||
|
spoiler_text: spoilerText,
|
||||||
|
visibility: visibility
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key of Object.keys(body)) {
|
||||||
|
let value = body[key]
|
||||||
|
if (!value || (Array.isArray(value) && !value.length)) {
|
||||||
|
delete body[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return post(url, body, auth(accessToken), {timeout: WRITE_TIMEOUT})
|
||||||
|
}
|
|
@ -15,6 +15,8 @@ function getTimelineUrlPath (timeline) {
|
||||||
}
|
}
|
||||||
if (timeline.startsWith('tag/')) {
|
if (timeline.startsWith('tag/')) {
|
||||||
return 'timelines/tag'
|
return 'timelines/tag'
|
||||||
|
} else if (timeline.startsWith('status/')) {
|
||||||
|
return 'statuses'
|
||||||
} else if (timeline.startsWith('account/')) {
|
} else if (timeline.startsWith('account/')) {
|
||||||
return 'accounts'
|
return 'accounts'
|
||||||
} else if (timeline.startsWith('list/')) {
|
} else if (timeline.startsWith('list/')) {
|
||||||
|
@ -22,12 +24,14 @@ function getTimelineUrlPath (timeline) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTimeline (instanceName, accessToken, timeline, maxId, since, limit) {
|
export function getTimeline (instanceName, accessToken, timeline, maxId, since) {
|
||||||
let timelineUrlName = getTimelineUrlPath(timeline)
|
let timelineUrlName = getTimelineUrlPath(timeline)
|
||||||
let url = `${basename(instanceName)}/api/v1/${timelineUrlName}`
|
let url = `${basename(instanceName)}/api/v1/${timelineUrlName}`
|
||||||
|
|
||||||
if (timeline.startsWith('tag/')) {
|
if (timeline.startsWith('tag/')) {
|
||||||
url += '/' + timeline.split('/').slice(-1)[0]
|
url += '/' + timeline.split('/').slice(-1)[0]
|
||||||
|
} else if (timeline.startsWith('status/')) {
|
||||||
|
url += '/' + timeline.split('/').slice(-1)[0] + '/context'
|
||||||
} else if (timeline.startsWith('account/')) {
|
} else if (timeline.startsWith('account/')) {
|
||||||
url += '/' + timeline.split('/').slice(-1)[0] + '/statuses'
|
url += '/' + timeline.split('/').slice(-1)[0] + '/statuses'
|
||||||
} else if (timeline.startsWith('list/')) {
|
} else if (timeline.startsWith('list/')) {
|
||||||
|
@ -43,15 +47,22 @@ export function getTimeline (instanceName, accessToken, timeline, maxId, since,
|
||||||
params.max_id = maxId
|
params.max_id = maxId
|
||||||
}
|
}
|
||||||
|
|
||||||
if (limit) {
|
|
||||||
params.limit = limit
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeline === 'local') {
|
if (timeline === 'local') {
|
||||||
params.local = true
|
params.local = true
|
||||||
}
|
}
|
||||||
|
|
||||||
url += '?' + paramsString(params)
|
url += '?' + paramsString(params)
|
||||||
|
|
||||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
if (timeline.startsWith('status/')) {
|
||||||
|
// special case - this is a list of descendents and ancestors
|
||||||
|
let statusUrl = `${basename(instanceName)}/api/v1/statuses/${timeline.split('/').slice(-1)[0]}`
|
||||||
|
return Promise.all([
|
||||||
|
get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT}),
|
||||||
|
get(statusUrl, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||||
|
]).then(res => {
|
||||||
|
return [].concat(res[0].ancestors).concat([res[1]]).concat(res[0].descendants)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||||
}
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { get, paramsString, DEFAULT_TIMEOUT } from '../_utils/ajax'
|
||||||
|
import { auth, basename } from './utils'
|
||||||
|
|
||||||
|
export function getVerifyCredentials (instanceName, accessToken) {
|
||||||
|
let url = `${basename(instanceName)}/api/v1/accounts/verify_credentials`
|
||||||
|
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccount (instanceName, accessToken, accountId) {
|
||||||
|
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}`
|
||||||
|
return get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRelationship (instanceName, accessToken, accountId) {
|
||||||
|
let url = `${basename(instanceName)}/api/v1/accounts/relationships`
|
||||||
|
url += '?' + paramsString({id: accountId})
|
||||||
|
let res = await get(url, auth(accessToken), {timeout: DEFAULT_TIMEOUT})
|
||||||
|
return res[0]
|
||||||
|
}
|
|
@ -1,9 +1,13 @@
|
||||||
|
const isLocalhost = !process.browser ||
|
||||||
|
location.hostname === 'localhost' ||
|
||||||
|
location.hostname === '127.0.0.1'
|
||||||
|
|
||||||
function targetIsLocalhost (instanceName) {
|
function targetIsLocalhost (instanceName) {
|
||||||
return instanceName.startsWith('localhost:') || instanceName.startsWith('127.0.0.1:')
|
return instanceName.startsWith('localhost:') || instanceName.startsWith('127.0.0.1:')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function basename (instanceName) {
|
export function basename (instanceName) {
|
||||||
if (targetIsLocalhost(instanceName)) {
|
if (isLocalhost && targetIsLocalhost(instanceName)) {
|
||||||
return `http://${instanceName}`
|
return `http://${instanceName}`
|
||||||
}
|
}
|
||||||
return `https://${instanceName}`
|
return `https://${instanceName}`
|
|
@ -32,9 +32,9 @@
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import LoadingPage from './LoadingPage.html'
|
import LoadingPage from '../_components/LoadingPage.html'
|
||||||
import AccountSearchResult from './search/AccountSearchResult.html'
|
import AccountSearchResult from '../_components/search/AccountSearchResult.html'
|
||||||
import { toast } from './toast/toast'
|
import { toast } from '../_utils/toast'
|
||||||
import { on } from '../_utils/eventBus'
|
import { on } from '../_utils/eventBus'
|
||||||
|
|
||||||
// TODO: paginate
|
// TODO: paginate
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.say('Error: ' + (e.name || '') + ' ' + (e.message || ''))
|
toast.say('Error: ' + (e.name || '') + ' ' + (e.message || ''))
|
||||||
} finally {
|
} finally {
|
||||||
this.set({ loading: false })
|
this.set({loading: false})
|
||||||
}
|
}
|
||||||
on('refreshAccountsList', this, () => this.refreshAccounts())
|
on('refreshAccountsList', this, () => this.refreshAccounts())
|
||||||
},
|
},
|
||||||
|
@ -71,4 +71,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<video
|
||||||
|
class="autoplay-video {className || ''}"
|
||||||
|
aria-label={ariaLabel || ''}
|
||||||
|
style="background-image: url({poster});"
|
||||||
|
{poster}
|
||||||
|
{width}
|
||||||
|
{height}
|
||||||
|
{src}
|
||||||
|
autoplay
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
webkit-playsinline
|
||||||
|
playsinline
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
.autoplay-video {
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,16 +1,13 @@
|
||||||
{#if error}
|
{#if error}
|
||||||
<svg class={computedClass} style={svgStyle} aria-hidden="true">
|
<svg class={computedClass} aria-hidden="true">
|
||||||
<use xlink:href="#fa-user" />
|
<use xlink:href="#fa-user" />
|
||||||
</svg>
|
</svg>
|
||||||
{:elseif $autoplayGifs}
|
{:elseif $autoplayGifs}
|
||||||
<LazyImage
|
<img
|
||||||
className={computedClass}
|
class={computedClass}
|
||||||
ariaHidden="true"
|
aria-hidden="true"
|
||||||
forceSize=true
|
|
||||||
alt=""
|
alt=""
|
||||||
src={account.avatar}
|
src={account.avatar}
|
||||||
{width}
|
|
||||||
{height}
|
|
||||||
on:imgLoad="set({loaded: true})"
|
on:imgLoad="set({loaded: true})"
|
||||||
on:imgLoadError="set({error: true})" />
|
on:imgLoadError="set({error: true})" />
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -20,8 +17,6 @@
|
||||||
alt=""
|
alt=""
|
||||||
src={account.avatar}
|
src={account.avatar}
|
||||||
staticSrc={account.avatar_static}
|
staticSrc={account.avatar_static}
|
||||||
{width}
|
|
||||||
{height}
|
|
||||||
{isLink}
|
{isLink}
|
||||||
on:imgLoad="set({loaded: true})"
|
on:imgLoad="set({loaded: true})"
|
||||||
on:imgLoadError="set({error: true})"
|
on:imgLoadError="set({error: true})"
|
||||||
|
@ -37,17 +32,48 @@
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.avatar.size-extra-small) {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.avatar.size-small) {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.avatar.size-medium) {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.avatar.size-big) {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
:global(.avatar.size-big) {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
svg.avatar {
|
svg.avatar {
|
||||||
fill: var(--deemphasized-text-color);
|
fill: var(--deemphasized-text-color);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
|
import { imgLoad, imgLoadError } from '../_utils/events'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import NonAutoplayImg from './NonAutoplayImg.html'
|
import NonAutoplayImg from './NonAutoplayImg.html'
|
||||||
import { classname } from '../_utils/classname'
|
import { classname } from '../_utils/classname'
|
||||||
import LazyImage from './LazyImage.html'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
events: {
|
||||||
|
imgLoad,
|
||||||
|
imgLoadError
|
||||||
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
className: void 0,
|
className: void 0,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
@ -56,30 +82,15 @@
|
||||||
}),
|
}),
|
||||||
store: () => store,
|
store: () => store,
|
||||||
computed: {
|
computed: {
|
||||||
computedClass: ({ className, loaded }) => (classname(
|
computedClass: ({ className, loaded, size }) => (classname(
|
||||||
'avatar',
|
'avatar',
|
||||||
className,
|
className,
|
||||||
loaded && 'loaded'
|
loaded && 'loaded',
|
||||||
)),
|
`size-${size}`
|
||||||
width: ({ size, $isMobileSize }) => {
|
))
|
||||||
switch (size) {
|
|
||||||
case 'extra-small':
|
|
||||||
return 24
|
|
||||||
case 'small':
|
|
||||||
return 48
|
|
||||||
case 'big':
|
|
||||||
return $isMobileSize ? 80 : 100
|
|
||||||
case 'medium':
|
|
||||||
default:
|
|
||||||
return 64
|
|
||||||
}
|
|
||||||
},
|
|
||||||
height: ({ width }) => width,
|
|
||||||
svgStyle: ({ width, height }) => `width: ${width}px; height: ${height}px;`
|
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
NonAutoplayImg,
|
NonAutoplayImg
|
||||||
LazyImage
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|