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
|
||||
node_modules
|
||||
/__sapper__
|
||||
.sapper
|
||||
yarn.lock
|
||||
templates/.*
|
||||
assets/*.css
|
||||
/mastodon
|
||||
/mastodon.log
|
||||
/src/template.html
|
||||
/static/*.css
|
||||
/static/robots.txt
|
||||
/static/inline-script.js.map
|
||||
/static/emoji-mart-all.json
|
||||
/src/inline-script/checksum.js
|
||||
yarn-error.log
|
||||
mastodon.log
|
||||
assets/robots.txt
|
||||
/inline-script-checksum.json
|
||||
|
|
75
.travis.yml
|
@ -1,53 +1,55 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "10"
|
||||
- "8"
|
||||
dist: trusty # needed for chrome headless
|
||||
sudo: required # needed for various sudo operations
|
||||
sudo: required # needed for chrome headless
|
||||
addons:
|
||||
chrome: stable
|
||||
postgresql: "10"
|
||||
apt:
|
||||
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-client-10
|
||||
- postgresql-contrib-10
|
||||
# the following are mastodon dependencies
|
||||
- imagemagick
|
||||
- libpq-dev
|
||||
- libxml2-dev
|
||||
- libxslt1-dev
|
||||
- file
|
||||
- g++
|
||||
- libprotobuf-dev
|
||||
- protobuf-compiler
|
||||
- redis-server
|
||||
- redis-tools
|
||||
- pkg-config nodejs
|
||||
- gcc
|
||||
- autoconf
|
||||
- bison
|
||||
- build-essential
|
||||
- libssl-dev
|
||||
- libyaml-dev
|
||||
- libreadline6-dev
|
||||
- zlib1g-dev
|
||||
- libncurses5-dev
|
||||
- libffi-dev
|
||||
- libgdbm3
|
||||
- libgdbm-dev
|
||||
- redis-tools
|
||||
- libidn11-dev
|
||||
- libicu-dev
|
||||
services:
|
||||
- redis-server
|
||||
before_install:
|
||||
- npm install -g npm@5
|
||||
- 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
|
||||
before_script:
|
||||
- yarn run lint
|
||||
- npm run lint
|
||||
- greenkeeper-lockfile-update
|
||||
after_script:
|
||||
- greenkeeper-lockfile-upload
|
||||
script: travis_retry yarn run $COMMAND
|
||||
install:
|
||||
- npm ci || npm i
|
||||
script: travis_retry npm run $COMMAND
|
||||
env:
|
||||
global:
|
||||
- PGPORT=5433
|
||||
|
@ -57,17 +59,16 @@ matrix:
|
|||
include:
|
||||
- env: BROWSER=chrome:headless COMMAND=test-browser-suite0
|
||||
- env: BROWSER=chrome:headless COMMAND=test-browser-suite1
|
||||
- env: COMMAND=test-unit
|
||||
- env: COMMAND=deploy-all-travis
|
||||
- env: COMMAND=deploy-dev-travis
|
||||
allow_failures:
|
||||
- env: COMMAND=deploy-all-travis
|
||||
- env: COMMAND=deploy-dev-travis
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^greenkeeper/.*$/
|
||||
cache:
|
||||
yarn: true
|
||||
bundler: true
|
||||
directories:
|
||||
- /home/travis/.rvm/
|
||||
- /home/travis/ffmpeg-static/
|
||||
- $HOME/.npm
|
||||
- $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
|
||||
|
||||
## 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:
|
||||
|
||||
yarn run dev
|
||||
npm run dev
|
||||
|
||||
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
|
||||
|
||||
Pinafore uses [JavaScript Standard Style](https://standardjs.com/).
|
||||
|
||||
Lint:
|
||||
|
||||
yarn run lint
|
||||
npm run lint
|
||||
|
||||
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
|
||||
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`.
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
BROWSER=chrome yarn run test-browser
|
||||
BROWSER=chrome:headless yarn run test-browser
|
||||
BROWSER=firefox yarn run test-browser
|
||||
BROWSER=firefox:headless yarn run test-browser
|
||||
BROWSER=safari yarn run test-browser
|
||||
BROWSER=edge yarn run test-browser
|
||||
BROWSER=chrome npm run test-browser
|
||||
BROWSER=chrome:headless npm run test-browser
|
||||
BROWSER=firefox npm run test-browser
|
||||
BROWSER=firefox:headless npm run test-browser
|
||||
BROWSER=safari npm run test-browser
|
||||
BROWSER=edge npm run test-browser
|
||||
|
||||
If the script isn't able to set up the Postgres database, try running:
|
||||
|
||||
sudo su - postgres
|
||||
|
||||
Then:
|
||||
|
||||
psql -d template1 -c "CREATE USER pinafore WITH PASSWORD 'pinafore' CREATEDB;"
|
||||
|
||||
### Testing in development mode
|
||||
## Testing in development mode
|
||||
|
||||
In separate terminals:
|
||||
|
||||
1\. Run a Mastodon dev server:
|
||||
|
||||
yarn run run-mastodon
|
||||
npm run run-mastodon
|
||||
|
||||
2\. Run a Pinafore dev server:
|
||||
|
||||
yarn run dev
|
||||
npm run dev
|
||||
|
||||
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)
|
||||
* `1xx-test-name.js`: tests that do modify the Mastodon database (read-write)
|
||||
## Writing tests
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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/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
|
||||
are some quirks, which are described below. This list of quirks is non-exhaustive.
|
||||
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 `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
|
||||
|
||||
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.
|
||||
Check `mastodon.log` if you have any issues.
|
12
Dockerfile
|
@ -7,17 +7,17 @@ ADD . /app
|
|||
|
||||
# Install updates and NodeJS+Dependencies
|
||||
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
|
||||
RUN npm i yarn -g
|
||||
# Upgrading NPM
|
||||
RUN npm i npm@latest -g
|
||||
|
||||
# Install Pinafore
|
||||
RUN yarn --pure-lockfile
|
||||
RUN yarn build
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
# Expose port 4002
|
||||
EXPOSE 4002
|
||||
|
||||
# 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.
|
||||
|
||||
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
|
||||
|
||||
|
@ -24,68 +24,51 @@ Compatible versions of each (Opera, Brave, Samsung, etc.) should be fine.
|
|||
### Goals
|
||||
|
||||
- Support the most common use cases
|
||||
- Small page weight
|
||||
- Fast even on low-end devices
|
||||
- Accessibility
|
||||
- Offline support in read-only mode
|
||||
- Fast even on low-end phones
|
||||
- Works offline in read-only mode
|
||||
- Progressive Web App features
|
||||
- Multi-instance support
|
||||
- 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
|
||||
- Serve as an alternative frontend tied to a particular instance
|
||||
- Support for non-English languages (i18n)
|
||||
- Works as an alternative frontend self-hosted by instances
|
||||
- Android/iOS apps (using Cordova or similar)
|
||||
- Support Pleroma/non-Mastodon backends
|
||||
- i18n
|
||||
- Offline search
|
||||
- Full emoji keyboard
|
||||
- Keyboard shortcuts
|
||||
|
||||
### Non-goals
|
||||
|
||||
- Supporting old browsers, proxy browsers, or text-based browsers
|
||||
- React Native / NativeScript / hybrid-native version
|
||||
- Android/iOS apps (using Cordova or similar)
|
||||
- Full functionality with JavaScript disabled
|
||||
- Emoji support beyond the built-in system emoji
|
||||
- Multi-column support
|
||||
- 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
|
||||
|
||||
Pinafore requires [Node.js](https://nodejs.org/en/) v8+ and [Yarn](https://yarnpkg.com).
|
||||
|
||||
To build Pinafore for production:
|
||||
|
||||
yarn --pure-lockfile
|
||||
yarn build
|
||||
PORT=4002 yarn start
|
||||
npm install
|
||||
npm run build
|
||||
PORT=4002 npm start
|
||||
|
||||
### Docker
|
||||
|
||||
To build a Docker image for production:
|
||||
To build a docker image for production:
|
||||
|
||||
docker build .
|
||||
docker run -d -p 4002:4002 [your-image]
|
||||
|
||||
Now Pinafore is running at `localhost:4002`.
|
||||
|
||||
### Updating
|
||||
|
||||
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.
|
||||
Pinafore requires [Node.js](https://nodejs.org/en/) v8+.
|
||||
|
||||
## Developing and testing
|
||||
|
||||
|
@ -95,9 +78,3 @@ how to run Pinafore in dev mode and run tests.
|
|||
## Changelog
|
||||
|
||||
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'
|
||||
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'
|
||||
#!/usr/bin/env node
|
||||
|
||||
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 inlineScriptPath = path.join(__dirname, '../src/inline-script/inline-script.js')
|
||||
let checksum = crypto.createHash('sha256').update(headScript).digest('base64')
|
||||
|
||||
let bundle = await rollup({
|
||||
input: inlineScriptPath,
|
||||
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 checksumFilepath = path.join(__dirname, '../inline-script-checksum.json')
|
||||
await writeFile(checksumFilepath, JSON.stringify({checksum}), 'utf8')
|
||||
|
||||
let { code, map } = output[0]
|
||||
|
||||
let fullCode = `${code}//# sourceMappingURL=/inline-script.js.map`
|
||||
let checksum = crypto.createHash('sha256').update(fullCode).digest('base64')
|
||||
|
||||
await writeFile(path.resolve(__dirname, '../src/inline-script/checksum.js'),
|
||||
`module.exports = ${JSON.stringify(checksum)}`, 'utf8')
|
||||
await writeFile(path.resolve(__dirname, '../static/inline-script.js.map'),
|
||||
map.toString(), 'utf8')
|
||||
|
||||
return '<script>' + fullCode + '</script>'
|
||||
let html2xxFilepath = path.join(__dirname, '../templates/2xx.html')
|
||||
let html2xxFile = await readFile(html2xxFilepath, 'utf8')
|
||||
html2xxFile = html2xxFile.replace(
|
||||
/<!-- 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(html2xxFilepath, html2xxFile, 'utf8')
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
|
@ -1,46 +1,73 @@
|
|||
import sass from 'node-sass'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { promisify } from 'util'
|
||||
import cssDedoupe from 'css-dedoupe'
|
||||
import { TextDecoder } from 'text-encoding'
|
||||
#!/usr/bin/env node
|
||||
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
const readdir = promisify(fs.readdir)
|
||||
const render = promisify(sass.render.bind(sass))
|
||||
const sass = require('node-sass')
|
||||
const chokidar = require('chokidar')
|
||||
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 defaultThemeScss = path.join(__dirname, '../src/scss/themes/_default.scss')
|
||||
const offlineThemeScss = path.join(__dirname, '../src/scss/themes/_offline.scss')
|
||||
const customScrollbarScss = path.join(__dirname, '../src/scss/custom-scrollbars.scss')
|
||||
const themesScssDir = path.join(__dirname, '../src/scss/themes')
|
||||
const assetsDir = path.join(__dirname, '../static')
|
||||
const globalScss = path.join(__dirname, '../scss/global.scss')
|
||||
const defaultThemeScss = path.join(__dirname, '../scss/themes/_default.scss')
|
||||
const offlineThemeScss = path.join(__dirname, '../scss/themes/_offline.scss')
|
||||
const html2xxFile = path.join(__dirname, '../templates/2xx.html')
|
||||
const scssDir = path.join(__dirname, '../scss')
|
||||
const themesScssDir = path.join(__dirname, '../scss/themes')
|
||||
const assetsDir = path.join(__dirname, '../assets')
|
||||
|
||||
async function renderCss (file) {
|
||||
return (await render({ file, outputStyle: 'compressed' })).css
|
||||
function doWatch () {
|
||||
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 () {
|
||||
let mainStyle = (await Promise.all([defaultThemeScss, globalScss].map(renderCss))).join('')
|
||||
let offlineStyle = (await renderCss(offlineThemeScss))
|
||||
let scrollbarStyle = (await renderCss(customScrollbarScss))
|
||||
let results = await Promise.all([
|
||||
render({file: defaultThemeScss, outputStyle: 'compressed'}),
|
||||
render({file: globalScss, outputStyle: 'compressed'}),
|
||||
render({file: offlineThemeScss, outputStyle: 'compressed'})
|
||||
])
|
||||
|
||||
return `<style>\n${mainStyle}</style>\n` +
|
||||
`<style media="only x" id="theOfflineStyle">\n${offlineStyle}</style>\n` +
|
||||
`<style media="all" id="theScrollbarStyle">\n${scrollbarStyle}</style>\n`
|
||||
let css = results.map(_ => _.css).join('')
|
||||
|
||||
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 () {
|
||||
let files = (await readdir(themesScssDir)).filter(file => !path.basename(file).startsWith('_'))
|
||||
await Promise.all(files.map(async file => {
|
||||
let css = await renderCss(path.join(themesScssDir, file))
|
||||
css = cssDedoupe(new TextDecoder('utf-8').decode(css)) // remove duplicate custom properties
|
||||
let res = await render({file: path.join(themesScssDir, file), outputStyle: 'compressed'})
|
||||
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 () {
|
||||
let [ result ] = await Promise.all([compileGlobalSass(), compileThemesSass()])
|
||||
return result
|
||||
async function main () {
|
||||
await Promise.all([compileGlobalSass(), compileThemesSass()])
|
||||
if (argv.watch) {
|
||||
doWatch()
|
||||
}
|
||||
}
|
||||
|
||||
Promise.resolve().then(main).catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import svgs from './svgs'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { promisify } from 'util'
|
||||
import SVGO from 'svgo'
|
||||
import $ from 'cheerio'
|
||||
#!/usr/bin/env node
|
||||
|
||||
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 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 filepath = path.join(__dirname, '../', svg.src)
|
||||
let content = await readFile(filepath, 'utf8')
|
||||
|
@ -18,9 +21,23 @@ export async function buildSvg () {
|
|||
let $symbol = $('<symbol></symbol>')
|
||||
.attr('id', svg.id)
|
||||
.attr('viewBox', `0 0 ${optimized.info.width} ${optimized.info.height}`)
|
||||
.append($('<title></title>').text(svg.title))
|
||||
.append($path)
|
||||
return $.xml($symbol)
|
||||
}))).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 { users } from '../tests/users'
|
||||
import { postStatus } from '../src/routes/_api/statuses'
|
||||
import { followAccount } from '../src/routes/_api/follow'
|
||||
import { favoriteStatus } from '../src/routes/_api/favorite'
|
||||
import { reblogStatus } from '../src/routes/_api/reblog'
|
||||
import { postStatus } from '../routes/_api/statuses'
|
||||
import { followAccount } from '../routes/_api/follow'
|
||||
import { favoriteStatus } from '../routes/_api/favorite'
|
||||
import { reblogStatus } from '../routes/_api/reblog'
|
||||
import fetch from 'node-fetch'
|
||||
import FileApi from 'file-api'
|
||||
import { pinStatus } from '../src/routes/_api/pin'
|
||||
import { submitMedia } from '../tests/submitMedia'
|
||||
import path from 'path'
|
||||
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.FormData = FileApi.FormData
|
||||
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 () {
|
||||
console.log('Restoring mastodon data...')
|
||||
let internalIdsToIds = {}
|
||||
for (let action of actions) {
|
||||
if (!action.post) {
|
||||
// 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))
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)) // delay so that notifications have proper order
|
||||
console.log(JSON.stringify(action))
|
||||
let accessToken = users[action.user].accessToken
|
||||
|
||||
if (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 mediaResponse = await submitMedia(accessToken, mediaItem, 'kitten')
|
||||
return mediaResponse.id
|
||||
}))
|
||||
let inReplyToId = inReplyTo && internalIdsToIds[inReplyTo]
|
||||
let status = await postStatus('localhost:3000', accessToken, text, inReplyToId, mediaIds,
|
||||
let status = await postStatus('localhost:3000', accessToken, text, inReplyTo, mediaIds,
|
||||
sensitive, spoiler, privacy || 'public')
|
||||
if (typeof internalId !== 'undefined') {
|
||||
internalIdsToIds[internalId] = status.id
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { restoreMastodonData } from './restore-mastodon-data'
|
||||
import { promisify } from 'util'
|
||||
import pify from 'pify'
|
||||
import childProcessPromise from 'child-process-promise'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
@ -8,13 +8,13 @@ import mkdirpCB from 'mkdirp'
|
|||
|
||||
const exec = childProcessPromise.exec
|
||||
const spawn = childProcessPromise.spawn
|
||||
const mkdirp = promisify(mkdirpCB)
|
||||
const stat = promisify(fs.stat)
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
const mkdirp = pify(mkdirpCB)
|
||||
const stat = pify(fs.stat.bind(fs))
|
||||
const writeFile = pify(fs.writeFile.bind(fs))
|
||||
const dir = __dirname
|
||||
|
||||
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_USER = 'pinafore'
|
||||
|
@ -43,7 +43,6 @@ async function cloneMastodon () {
|
|||
} catch (e) {
|
||||
console.log('Cloning mastodon...')
|
||||
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 writeFile(path.join(dir, '../mastodon/.env'), envFile, 'utf8')
|
||||
}
|
||||
|
@ -57,24 +56,24 @@ async function setupMastodonDatabase () {
|
|||
try {
|
||||
await exec(`dropdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
||||
cwd: mastodonDir,
|
||||
env: Object.assign({ PGPASSWORD: DB_PASS }, process.env)
|
||||
env: Object.assign({PGPASSWORD: DB_PASS}, process.env)
|
||||
})
|
||||
} catch (e) { /* ignore */ }
|
||||
await exec(`createdb -h 127.0.0.1 -U ${DB_USER} -w ${DB_NAME}`, {
|
||||
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}"`, {
|
||||
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')
|
||||
await mkdirp(systemDir)
|
||||
await exec(`tar -xzf "${tgzFile}"`, { cwd: systemDir })
|
||||
await exec(`tar -xzf "${tgzFile}"`, {cwd: systemDir})
|
||||
}
|
||||
|
||||
async function runMastodon () {
|
||||
|
@ -96,20 +95,12 @@ async function runMastodon () {
|
|||
'yarn --pure-lockfile'
|
||||
]
|
||||
|
||||
const installedFile = path.join(mastodonDir, 'installed.txt')
|
||||
try {
|
||||
await stat(installedFile)
|
||||
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')
|
||||
for (let cmd of cmds) {
|
||||
console.log(cmd)
|
||||
await exec(cmd, {cwd, env})
|
||||
}
|
||||
const promise = spawn('foreman', ['start'], { cwd, env })
|
||||
const log = fs.createWriteStream('mastodon.log', { flags: 'a' })
|
||||
const promise = spawn('foreman', ['start'], {cwd, env})
|
||||
const log = fs.createWriteStream('mastodon.log', {flags: 'a'})
|
||||
childProc = promise.childProcess
|
||||
childProc.stdout.pipe(log)
|
||||
childProc.stderr.pipe(log)
|
||||
|
|
|
@ -2,39 +2,20 @@
|
|||
|
||||
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
|
||||
fi
|
||||
|
||||
# install ruby
|
||||
source "$HOME/.rvm/scripts/rvm"
|
||||
rvm install 2.6.0
|
||||
rvm use 2.6.0
|
||||
rvm install 2.5.1
|
||||
rvm use 2.5.1
|
||||
|
||||
# fix for redis IPv6 issue
|
||||
# https://travis-ci.community/t/trusty-environment-redis-server-not-starting-with-redis-tools-installed/650/2
|
||||
sudo sed -e 's/^bind.*/bind 127.0.0.1/' /etc/redis/redis.conf > redis.conf
|
||||
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
|
||||
sudo -E add-apt-repository -y ppa:mc3man/trusty-media
|
||||
sudo -E apt-get update
|
||||
sudo -E apt-get install -y ffmpeg
|
||||
ruby --version
|
||||
node --version
|
||||
yarn --version
|
||||
npm --version
|
||||
postgres --version
|
||||
redis-server --version
|
||||
ffmpeg -version
|
||||
|
|
83
bin/svgs.js
|
@ -1,47 +1,40 @@
|
|||
module.exports = [
|
||||
{ id: 'pinafore-logo', src: 'src/static/sailboat.svg' },
|
||||
{ id: 'fa-bell', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell.svg' },
|
||||
{ id: 'fa-users', src: 'src/thirdparty/font-awesome-svg-png/white/svg/users.svg' },
|
||||
{ id: 'fa-globe', src: 'src/thirdparty/font-awesome-svg-png/white/svg/globe.svg' },
|
||||
{ id: 'fa-gear', src: 'src/thirdparty/font-awesome-svg-png/white/svg/gear.svg' },
|
||||
{ id: 'fa-reply', src: 'src/thirdparty/font-awesome-svg-png/white/svg/reply.svg' },
|
||||
{ id: 'fa-reply-all', src: 'src/thirdparty/font-awesome-svg-png/white/svg/reply-all.svg' },
|
||||
{ id: 'fa-retweet', src: 'src/thirdparty/font-awesome-svg-png/white/svg/retweet.svg' },
|
||||
{ id: 'fa-star', src: 'src/thirdparty/font-awesome-svg-png/white/svg/star.svg' },
|
||||
{ id: 'fa-star-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/star-o.svg' },
|
||||
{ id: 'fa-ellipsis-h', src: 'src/thirdparty/font-awesome-svg-png/white/svg/ellipsis-h.svg' },
|
||||
{ id: 'fa-spinner', src: 'src/thirdparty/font-awesome-svg-png/white/svg/spinner.svg' },
|
||||
{ id: 'fa-user', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user.svg' },
|
||||
{ id: 'fa-play-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/play-circle.svg' },
|
||||
{ id: 'fa-eye', src: 'src/thirdparty/font-awesome-svg-png/white/svg/eye.svg' },
|
||||
{ id: 'fa-eye-slash', src: 'src/thirdparty/font-awesome-svg-png/white/svg/eye-slash.svg' },
|
||||
{ id: 'fa-lock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/lock.svg' },
|
||||
{ id: 'fa-unlock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/unlock.svg' },
|
||||
{ id: 'fa-envelope', src: 'src/thirdparty/font-awesome-svg-png/white/svg/envelope.svg' },
|
||||
{ id: 'fa-user-times', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user-times.svg' },
|
||||
{ id: 'fa-user-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user-plus.svg' },
|
||||
{ id: 'fa-external-link', src: 'src/thirdparty/font-awesome-svg-png/white/svg/external-link.svg' },
|
||||
{ id: 'fa-search', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search.svg' },
|
||||
{ id: 'fa-comments', src: 'src/thirdparty/font-awesome-svg-png/white/svg/comments.svg' },
|
||||
{ id: 'fa-paperclip', src: 'src/thirdparty/font-awesome-svg-png/white/svg/paperclip.svg' },
|
||||
{ id: 'fa-thumb-tack', src: 'src/thirdparty/font-awesome-svg-png/white/svg/thumb-tack.svg' },
|
||||
{ id: 'fa-bars', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bars.svg' },
|
||||
{ id: 'fa-ban', src: 'src/thirdparty/font-awesome-svg-png/white/svg/ban.svg' },
|
||||
{ id: 'fa-camera', src: 'src/thirdparty/font-awesome-svg-png/white/svg/camera.svg' },
|
||||
{ id: 'fa-smile', src: 'src/thirdparty/font-awesome-svg-png/white/svg/smile-o.svg' },
|
||||
{ id: 'fa-exclamation-triangle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/exclamation-triangle.svg' },
|
||||
{ id: 'fa-check', src: 'src/thirdparty/font-awesome-svg-png/white/svg/check.svg' },
|
||||
{ id: 'fa-trash', src: 'src/thirdparty/font-awesome-svg-png/white/svg/trash-o.svg' },
|
||||
{ id: 'fa-hourglass', src: 'src/thirdparty/font-awesome-svg-png/white/svg/hourglass.svg' },
|
||||
{ id: 'fa-pencil', src: 'src/thirdparty/font-awesome-svg-png/white/svg/pencil.svg' },
|
||||
{ id: 'fa-times', src: 'src/thirdparty/font-awesome-svg-png/white/svg/times.svg' },
|
||||
{ id: 'fa-volume-off', src: 'src/thirdparty/font-awesome-svg-png/white/svg/volume-off.svg' },
|
||||
{ id: 'fa-volume-up', src: 'src/thirdparty/font-awesome-svg-png/white/svg/volume-up.svg' },
|
||||
{ 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' }
|
||||
{id: 'pinafore-logo', src: 'original-assets/sailboat.svg', title: 'Home'},
|
||||
{id: 'fa-bell', src: 'node_modules/font-awesome-svg-png/white/svg/bell.svg', title: 'Notifications'},
|
||||
{id: 'fa-users', src: 'node_modules/font-awesome-svg-png/white/svg/users.svg', title: 'Local'},
|
||||
{id: 'fa-globe', src: 'node_modules/font-awesome-svg-png/white/svg/globe.svg', title: 'Federated'},
|
||||
{id: 'fa-gear', src: 'node_modules/font-awesome-svg-png/white/svg/gear.svg', title: 'Settings'},
|
||||
{id: 'fa-reply', src: 'node_modules/font-awesome-svg-png/white/svg/reply.svg', title: 'Reply'},
|
||||
{id: 'fa-reply-all', src: 'node_modules/font-awesome-svg-png/white/svg/reply-all.svg', title: 'Reply to thread'},
|
||||
{id: 'fa-retweet', src: 'node_modules/font-awesome-svg-png/white/svg/retweet.svg', title: 'Boost'},
|
||||
{id: 'fa-star', src: 'node_modules/font-awesome-svg-png/white/svg/star.svg', title: 'Favorite'},
|
||||
{id: 'fa-ellipsis-h', src: 'node_modules/font-awesome-svg-png/white/svg/ellipsis-h.svg', title: 'More'},
|
||||
{id: 'fa-spinner', src: 'node_modules/font-awesome-svg-png/white/svg/spinner.svg', title: 'Spinner'},
|
||||
{id: 'fa-user', src: 'node_modules/font-awesome-svg-png/white/svg/user.svg', title: 'Empty user profile'},
|
||||
{id: 'fa-play-circle', src: 'node_modules/font-awesome-svg-png/white/svg/play-circle.svg', title: 'Play'},
|
||||
{id: 'fa-eye', src: 'node_modules/font-awesome-svg-png/white/svg/eye.svg', title: 'Show Sensitive Content'},
|
||||
{id: 'fa-eye-slash', src: 'node_modules/font-awesome-svg-png/white/svg/eye-slash.svg', title: 'Hide Sensitive Content'},
|
||||
{id: 'fa-lock', src: 'node_modules/font-awesome-svg-png/white/svg/lock.svg', title: 'Locked'},
|
||||
{id: 'fa-unlock', src: 'node_modules/font-awesome-svg-png/white/svg/unlock.svg', title: 'Unlocked'},
|
||||
{id: 'fa-envelope', src: 'node_modules/font-awesome-svg-png/white/svg/envelope.svg', title: 'Sealed Envelope'},
|
||||
{id: 'fa-user-times', src: 'node_modules/font-awesome-svg-png/white/svg/user-times.svg', title: 'Stop Following'},
|
||||
{id: 'fa-user-plus', src: 'node_modules/font-awesome-svg-png/white/svg/user-plus.svg', title: 'Follow'},
|
||||
{id: 'fa-external-link', src: 'node_modules/font-awesome-svg-png/white/svg/external-link.svg', title: 'External Link'},
|
||||
{id: 'fa-search', src: 'node_modules/font-awesome-svg-png/white/svg/search.svg', title: 'Search'},
|
||||
{id: 'fa-comments', src: 'node_modules/font-awesome-svg-png/white/svg/comments.svg', title: 'Conversations'},
|
||||
{id: 'fa-paperclip', src: 'node_modules/font-awesome-svg-png/white/svg/paperclip.svg', title: 'Paperclip'},
|
||||
{id: 'fa-thumb-tack', src: 'node_modules/font-awesome-svg-png/white/svg/thumb-tack.svg', title: 'Thumbtack'},
|
||||
{id: 'fa-bars', src: 'node_modules/font-awesome-svg-png/white/svg/bars.svg', title: 'List'},
|
||||
{id: 'fa-ban', src: 'node_modules/font-awesome-svg-png/white/svg/ban.svg', title: 'Ban'},
|
||||
{id: 'fa-camera', src: 'node_modules/font-awesome-svg-png/white/svg/camera.svg', title: 'Add media'},
|
||||
{id: 'fa-smile', src: 'node_modules/font-awesome-svg-png/white/svg/smile-o.svg', title: 'Custom emoji'},
|
||||
{id: 'fa-exclamation-triangle', src: 'node_modules/font-awesome-svg-png/white/svg/exclamation-triangle.svg', title: 'Content warning'},
|
||||
{id: 'fa-check', src: 'node_modules/font-awesome-svg-png/white/svg/check.svg', title: 'Check'},
|
||||
{id: 'fa-trash', src: 'node_modules/font-awesome-svg-png/white/svg/trash-o.svg', title: 'Delete'},
|
||||
{id: 'fa-hourglass', src: 'node_modules/font-awesome-svg-png/white/svg/hourglass.svg', title: 'Follow requested'},
|
||||
{id: 'fa-pencil', src: 'node_modules/font-awesome-svg-png/white/svg/pencil.svg', title: 'Compose'},
|
||||
{id: 'fa-times', src: 'node_modules/font-awesome-svg-png/white/svg/times.svg', title: 'Close'},
|
||||
{id: 'fa-volume-off', src: 'node_modules/font-awesome-svg-png/white/svg/volume-off.svg', title: 'Mute'},
|
||||
{id: 'fa-volume-up', src: 'node_modules/font-awesome-svg-png/white/svg/volume-up.svg', title: 'Unmute'},
|
||||
{id: 'fa-link', src: 'node_modules/font-awesome-svg-png/white/svg/link.svg', title: 'Link'}
|
||||
]
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
import fetch from 'node-fetch'
|
||||
import { actions } from './mastodon-data'
|
||||
|
||||
const numStatuses = actions
|
||||
.map(_ => _.post || _.boost)
|
||||
.filter(Boolean)
|
||||
.filter(_ => _.privacy !== 'direct')
|
||||
.length
|
||||
const numStatuses = actions.filter(_ => _.post || _.boost).length
|
||||
|
||||
async function waitForMastodonData () {
|
||||
while (true) {
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
## Theming
|
||||
|
||||
This document describes how to write your own theme for Pinafore.
|
||||
|
||||
First, create a file `scss/themes/foobar.scss`, write some SCSS inside and add
|
||||
the following at the bottom of `scss/themes/foobar.scss`.
|
||||
Create a file `scss/themes/foobar.scss`, write some SCSS inside and add the following at the bottom of `scss/themes/foobar.scss`.
|
||||
```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`
|
||||
> while the all CSS Custom Properties available are listed in `scss/themes/_base.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`.
|
||||
|
||||
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
|
||||
const themes = [
|
||||
...
|
||||
{
|
||||
name: 'foobar',
|
||||
label: 'Foobar', // user-visible name
|
||||
color: 'magenta', // main theme color
|
||||
dark: true // whether it's a dark theme or not
|
||||
label: 'Foobar'
|
||||
}
|
||||
]
|
||||
|
||||
export { themes }
|
||||
```
|
||||
|
||||
Start the development server (`yarn 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).
|
||||
Add your theme in `inline-script.js`.
|
||||
```js
|
||||
window.__themeColors = {
|
||||
'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",
|
||||
"description": "Alternative web client for Mastodon",
|
||||
"version": "1.0.1",
|
||||
"version": "0.5.2",
|
||||
"scripts": {
|
||||
"lint": "standard && standard --plugin html 'src/routes/**/*.html'",
|
||||
"lint-fix": "standard --fix && standard --fix --plugin html 'src/routes/**/*.html'",
|
||||
"dev": "run-s build-template-html build-third-party-assets serve-dev",
|
||||
"serve-dev": "run-p --race build-template-html-watch sapper-dev",
|
||||
"sapper-dev": "cross-env NODE_ENV=development PORT=4002 sapper dev",
|
||||
"sapper-prod": "cross-env PORT=4002 node __sapper__/build",
|
||||
"before-build": "run-s build-template-html build-third-party-assets",
|
||||
"build": "cross-env NODE_ENV=production run-s build-steps",
|
||||
"build-steps": "run-s before-build sapper-build",
|
||||
"lint": "standard && standard --plugin html 'routes/**/*.html'",
|
||||
"lint-fix": "standard --fix && standard --fix --plugin html 'routes/**/*.html'",
|
||||
"dev": "run-s build-svg build-inline-script serve-dev",
|
||||
"serve-dev": "run-p --race build-sass-watch serve",
|
||||
"serve": "node server.js",
|
||||
"build": "cross-env NODE_ENV=production npm run build-steps",
|
||||
"build-steps": "run-s globalize-css build-sass build-svg build-inline-script sapper-build deglobalize-css",
|
||||
"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-template-html": "node -r esm ./bin/build-template-html.js",
|
||||
"build-template-html-watch": "node -r esm ./bin/build-template-html.js --watch",
|
||||
"build-third-party-assets": "node -r esm ./bin/build-third-party-assets.js",
|
||||
"build-svg": "node ./bin/build-svg.js",
|
||||
"build-inline-script": "node ./bin/build-inline-script.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",
|
||||
"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-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",
|
||||
|
@ -29,87 +28,82 @@
|
|||
"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-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-data": "node -r esm bin/wait-for-mastodon-data.js",
|
||||
"deploy-prod": "DEPLOY_TYPE=prod ./bin/deploy.sh",
|
||||
"deploy-dev": "DEPLOY_TYPE=dev ./bin/deploy.sh",
|
||||
"deploy-all-travis": "./bin/deploy-all-travis.sh",
|
||||
"backup-mastodon-data": "./bin/backup-mastodon-data.sh",
|
||||
"sapper-export": "sapper export",
|
||||
"print-export-info": "node ./bin/print-export-info.js",
|
||||
"export-steps": "run-s before-build sapper-export print-export-info",
|
||||
"export": "cross-env NODE_ENV=production run-s export-steps"
|
||||
"globalize-css": "node ./bin/globalize-css.js",
|
||||
"deglobalize-css": "node ./bin/globalize-css.js --reverse",
|
||||
"stage-dev": "printf 'User-agent: *\nDisallow: /' > assets/robots.txt",
|
||||
"stage-prod": "rm -f assets/robots.txt",
|
||||
"launch": "now -e SAPPER_TIMESTAMP=$(date +%s%3N) --team nolanlawson && sleep 60",
|
||||
"launch-travis": "now -e SAPPER_TIMESTAMP=$(date +%s%3N) --team nolanlawson --token $NOW_TOKEN && sleep 60",
|
||||
"alias-prod": "now alias pinafore.social --team nolanlawson",
|
||||
"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": {
|
||||
"@gamestdio/websocket": "^0.2.8",
|
||||
"@webcomponents/custom-elements": "^1.2.1",
|
||||
"@gamestdio/websocket": "^0.2.7",
|
||||
"a11y-dialog": "^4.0.1",
|
||||
"browserslist": "^4.0.2",
|
||||
"cheerio": "^1.0.0-rc.2",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"chokidar": "^2.0.4",
|
||||
"circular-dependency-plugin": "^5.0.2",
|
||||
"clean-css": "^4.2.1",
|
||||
"compression": "^1.7.3",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-dedoupe": "^0.1.1",
|
||||
"css-loader": "^2.1.0",
|
||||
"emoji-mart": "github:nolanlawson/emoji-mart#for-pinafore-1",
|
||||
"emoji-regex": "^7.0.3",
|
||||
"encoding": "^0.1.12",
|
||||
"css-loader": "^1.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"esm": "^3.1.4",
|
||||
"events-light": "^1.0.5",
|
||||
"express": "^4.16.4",
|
||||
"esm": "^3.0.77",
|
||||
"events": "^3.0.0",
|
||||
"express": "^4.16.3",
|
||||
"fg-loadcss": "^2.0.1",
|
||||
"file-api": "^0.10.4",
|
||||
"file-drop-element": "0.0.9",
|
||||
"form-data": "^2.3.3",
|
||||
"glob": "^7.1.3",
|
||||
"helmet": "^3.15.0",
|
||||
"idb-keyval": "^3.1.0",
|
||||
"font-awesome-svg-png": "^1.2.2",
|
||||
"form-data": "^2.3.2",
|
||||
"glob": "^7.1.2",
|
||||
"helmet": "^3.13.0",
|
||||
"indexeddb-getall-shim": "^1.3.5",
|
||||
"inferno-compat": "^7.1.0",
|
||||
"intersection-observer": "^0.5.1",
|
||||
"localstorage-memory": "^1.0.3",
|
||||
"lodash-es": "^4.17.11",
|
||||
"intersection-observer": "^0.5.0",
|
||||
"lodash-es": "^4.17.10",
|
||||
"lodash-webpack-plugin": "^0.11.5",
|
||||
"mini-css-extract-plugin": "^0.4.1",
|
||||
"mkdirp": "^0.5.1",
|
||||
"node-fetch": "^2.3.0",
|
||||
"node-sass": "^4.11.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"node-fetch": "^2.2.0",
|
||||
"node-sass": "^4.9.3",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.0",
|
||||
"p-any": "^1.1.0",
|
||||
"page-lifecycle": "^0.1.1",
|
||||
"performance-now": "^2.1.0",
|
||||
"pinch-zoom-element": "^1.1.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"quick-lru": "^2.0.0",
|
||||
"remount": "^0.9.3",
|
||||
"pify": "^4.0.0",
|
||||
"quick-lru": "^1.1.0",
|
||||
"requestidlecallback": "^0.3.0",
|
||||
"rollup": "^1.1.2",
|
||||
"rollup-plugin-replace": "^2.1.0",
|
||||
"rollup-plugin-terser": "^4.0.3",
|
||||
"sapper": "^0.25.0",
|
||||
"sapper": "github:nolanlawson/sapper#for-pinafore-7",
|
||||
"serve-static": "^1.13.2",
|
||||
"shrink-ray-current": "^2.1.2",
|
||||
"stringz": "^1.0.0",
|
||||
"svelte": "^2.16.0",
|
||||
"style-loader": "^0.22.1",
|
||||
"svelte": "^2.11.0",
|
||||
"svelte-extras": "^2.0.2",
|
||||
"svelte-loader": "^2.12.0",
|
||||
"svelte-loader": "^2.10.1",
|
||||
"svelte-transitions": "^1.2.0",
|
||||
"svgo": "^1.1.1",
|
||||
"terser-webpack-plugin": "^1.2.1",
|
||||
"text-encoding": "^0.7.0",
|
||||
"svgo": "^1.0.5",
|
||||
"timeago.js": "^3.0.2",
|
||||
"tiny-queue": "^0.2.1",
|
||||
"uuid": "^3.3.2",
|
||||
"uglifyjs-webpack-plugin": "^1.3.0",
|
||||
"web-animations-js": "^2.3.1",
|
||||
"webpack": "^4.29.0",
|
||||
"webpack-bundle-analyzer": "^3.0.3"
|
||||
"webpack": "^4.16.5",
|
||||
"webpack-bundle-analyzer": "^2.13.1",
|
||||
"yargs": "^12.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"assert": "^1.4.1",
|
||||
"eslint-plugin-html": "^5.0.0",
|
||||
"mocha": "^5.2.0",
|
||||
"now": "^13.1.2",
|
||||
"standard": "^12.0.1",
|
||||
"testcafe": "^1.0.0"
|
||||
"eslint-plugin-html": "^4.0.5",
|
||||
"now": "^11.3.10",
|
||||
"standard": "^11.0.1",
|
||||
"testcafe": "^0.21.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
|
@ -142,18 +136,12 @@
|
|||
"btoa",
|
||||
"Blob",
|
||||
"Element",
|
||||
"Image",
|
||||
"NotificationEvent",
|
||||
"NodeList",
|
||||
"DOMParser",
|
||||
"CSS",
|
||||
"customElements"
|
||||
"Image"
|
||||
],
|
||||
"ignore": [
|
||||
"dist",
|
||||
"src/routes/_utils/asyncModules.js",
|
||||
"src/routes/_utils/asyncPolyfills.js",
|
||||
"src/routes/_components/dialog/asyncDialogs.js"
|
||||
"routes/_utils/asyncModules.js",
|
||||
"routes/_components/dialog/asyncDialogs.js"
|
||||
]
|
||||
},
|
||||
"esm": {
|
||||
|
@ -166,26 +154,27 @@
|
|||
"NODE_ENV": "production"
|
||||
},
|
||||
"files": [
|
||||
"assets",
|
||||
"bin",
|
||||
"inline-script.js",
|
||||
"original-static",
|
||||
"original-assets",
|
||||
"routes",
|
||||
"scss",
|
||||
"src",
|
||||
"src-build",
|
||||
"static",
|
||||
"templates",
|
||||
"package.json",
|
||||
"thirdparty",
|
||||
"webpack",
|
||||
"webpack.config.js",
|
||||
"yarn.lock"
|
||||
"package-lock.json",
|
||||
"server.js",
|
||||
"inline-script.js",
|
||||
"webpack.client.config.js",
|
||||
"webpack.server.config.js"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^10.0.0"
|
||||
"node": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"greenkeeper": {
|
||||
"ignore": [
|
||||
"sapper"
|
||||
"sapper",
|
||||
"a11y-dialog"
|
||||
]
|
||||
},
|
||||
"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 { getInstanceInfo } from '../_api/instance'
|
||||
import { goto } from '../../../__sapper__/client'
|
||||
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine'
|
||||
import { goto } from 'sapper/runtime.js'
|
||||
import { switchToTheme } from '../_utils/themeEngine'
|
||||
import { store } from '../_store/store'
|
||||
import { updateVerifyCredentialsForInstance } from './instances'
|
||||
import { updateCustomEmojiForInstance } from './emoji'
|
||||
import { database } from '../_database/database'
|
||||
import { setInstanceInfo as setInstanceInfoInDatabase } from '../_database/meta'
|
||||
|
||||
const REDIRECT_URI = (typeof location !== 'undefined'
|
||||
? location.origin : 'https://pinafore.social') + '/settings/instances/add'
|
||||
|
@ -14,13 +14,12 @@ async function redirectToOauth () {
|
|||
let { instanceNameInSearch, loggedInInstances } = store.get()
|
||||
instanceNameInSearch = instanceNameInSearch.replace(/^https?:\/\//, '').replace(/\/$/, '').replace('/$', '').toLowerCase()
|
||||
if (Object.keys(loggedInInstances).includes(instanceNameInSearch)) {
|
||||
let err = new Error(`You've already logged in to ${instanceNameInSearch}`)
|
||||
err.knownError = true
|
||||
throw err
|
||||
store.set({logInToInstanceError: `You've already logged in to ${instanceNameInSearch}`})
|
||||
return
|
||||
}
|
||||
let registrationPromise = registerApplication(instanceNameInSearch, REDIRECT_URI)
|
||||
let instanceInfo = await getInstanceInfo(instanceNameInSearch)
|
||||
await database.setInstanceInfo(instanceNameInSearch, instanceInfo) // cache for later
|
||||
await setInstanceInfoInDatabase(instanceNameInSearch, instanceInfo) // cache for later
|
||||
let instanceData = await registrationPromise
|
||||
store.set({
|
||||
currentRegisteredInstanceName: instanceNameInSearch,
|
||||
|
@ -45,17 +44,16 @@ export async function logInToInstance () {
|
|||
} catch (err) {
|
||||
console.error(err)
|
||||
let error = `${err.message || err.name}. ` +
|
||||
(err.knownError ? '' : (navigator.onLine
|
||||
? `Is this a valid Mastodon instance? Is a browser extension
|
||||
blocking the request? Are you in private browsing mode?`
|
||||
: `Are you offline?`))
|
||||
(navigator.onLine
|
||||
? `Is this a valid Mastodon instance? Is a browser extension blocking the request?`
|
||||
: `Are you offline?`)
|
||||
let { instanceNameInSearch } = store.get()
|
||||
store.set({
|
||||
logInToInstanceError: error,
|
||||
logInToInstanceErrorForText: instanceNameInSearch
|
||||
})
|
||||
} finally {
|
||||
store.set({ logInToInstanceLoading: false })
|
||||
store.set({logInToInstanceLoading: false})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,7 +67,7 @@ async function registerNewInstance (code) {
|
|||
REDIRECT_URI
|
||||
)
|
||||
let { loggedInInstances, loggedInInstancesInOrder, instanceThemes } = store.get()
|
||||
instanceThemes[currentRegisteredInstanceName] = DEFAULT_THEME
|
||||
instanceThemes[currentRegisteredInstanceName] = 'default'
|
||||
loggedInInstances[currentRegisteredInstanceName] = instanceData
|
||||
if (!loggedInInstancesInOrder.includes(currentRegisteredInstanceName)) {
|
||||
loggedInInstancesInOrder.push(currentRegisteredInstanceName)
|
||||
|
@ -84,7 +82,7 @@ async function registerNewInstance (code) {
|
|||
instanceThemes: instanceThemes
|
||||
})
|
||||
store.save()
|
||||
switchToTheme(DEFAULT_THEME)
|
||||
switchToTheme('default')
|
||||
// fire off these requests so they're cached
|
||||
/* no await */ updateVerifyCredentialsForInstance(currentRegisteredInstanceName)
|
||||
/* no await */ updateCustomEmojiForInstance(currentRegisteredInstanceName)
|
||||
|
@ -93,11 +91,11 @@ async function registerNewInstance (code) {
|
|||
|
||||
export async function handleOauthCode (code) {
|
||||
try {
|
||||
store.set({ logInToInstanceLoading: true })
|
||||
store.set({logInToInstanceLoading: true})
|
||||
await registerNewInstance(code)
|
||||
} 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 {
|
||||
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 { store } from '../_store/store'
|
||||
import uniqBy from 'lodash-es/uniqBy'
|
||||
import uniq from 'lodash-es/uniq'
|
||||
import isEqual from 'lodash-es/isEqual'
|
||||
import { database } from '../_database/database'
|
||||
import { concat } from '../_utils/arrays'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
import {
|
||||
insertTimelineItems as insertTimelineItemsInDatabase
|
||||
} from '../_database/timelines/insertion'
|
||||
import { runMediumPriorityTask } from '../_utils/runMediumPriorityTask'
|
||||
|
||||
const STREAMING_THROTTLE_DELAY = 3000
|
||||
|
||||
function getExistingItemIdsSet (instanceName, timelineName) {
|
||||
let timelineItemIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds') || []
|
||||
|
@ -25,31 +29,14 @@ async function insertUpdatesIntoTimeline (instanceName, timelineName, updates) {
|
|||
return
|
||||
}
|
||||
|
||||
await database.insertTimelineItems(instanceName, timelineName, updates)
|
||||
await insertTimelineItemsInDatabase(instanceName, timelineName, updates)
|
||||
|
||||
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)) {
|
||||
console.log('adding ', (newItemIdsToAdd.length - itemIdsToAdd.length),
|
||||
'items to itemIdsToAdd for timeline', timelineName)
|
||||
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)
|
||||
)
|
||||
store.setForTimeline(instanceName, timelineName, {itemIdsToAdd: newItemIdsToAdd})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,20 +46,21 @@ async function insertUpdatesIntoThreads (instanceName, updates) {
|
|||
}
|
||||
|
||||
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') || []
|
||||
let validUpdates = updates.filter(isValidStatusForThread(thread, timelineName, itemIdsToAdd))
|
||||
if (!validUpdates.length) {
|
||||
for (let timelineName of Object.keys(threads)) {
|
||||
let thread = threads[timelineName]
|
||||
let updatesForThisThread = updates.filter(
|
||||
status => thread.includes(status.in_reply_to_id) && !thread.includes(status.id)
|
||||
)
|
||||
if (!updatesForThisThread.length) {
|
||||
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)) {
|
||||
console.log('adding ', (newItemIdsToAdd.length - itemIdsToAdd.length),
|
||||
'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')
|
||||
if (freshUpdates && freshUpdates.length) {
|
||||
let updates = freshUpdates.slice()
|
||||
store.setForTimeline(instanceName, timelineName, { freshUpdates: [] })
|
||||
store.setForTimeline(instanceName, timelineName, {freshUpdates: []})
|
||||
|
||||
await Promise.all([
|
||||
insertUpdatesIntoTimeline(instanceName, timelineName, updates),
|
||||
|
@ -92,11 +80,11 @@ async function processFreshUpdates (instanceName, timelineName) {
|
|||
stop('processFreshUpdates')
|
||||
}
|
||||
|
||||
function lazilyProcessFreshUpdates (instanceName, timelineName) {
|
||||
scheduleIdleTask(() => {
|
||||
const lazilyProcessFreshUpdates = throttle((instanceName, timelineName) => {
|
||||
runMediumPriorityTask(() => {
|
||||
/* no await */ processFreshUpdates(instanceName, timelineName)
|
||||
})
|
||||
}
|
||||
}, STREAMING_THROTTLE_DELAY)
|
||||
|
||||
export function addStatusOrNotification (instanceName, timelineName, newStatusOrNotification) {
|
||||
addStatusesOrNotifications(instanceName, timelineName, [newStatusOrNotification])
|
||||
|
@ -105,8 +93,8 @@ export function addStatusOrNotification (instanceName, timelineName, newStatusOr
|
|||
export function addStatusesOrNotifications (instanceName, timelineName, newStatusesOrNotifications) {
|
||||
console.log('addStatusesOrNotifications', Date.now())
|
||||
let freshUpdates = store.getForTimeline(instanceName, timelineName, 'freshUpdates') || []
|
||||
freshUpdates = concat(freshUpdates, newStatusesOrNotifications)
|
||||
freshUpdates = [].concat(freshUpdates).concat(newStatusesOrNotifications)
|
||||
freshUpdates = uniqBy(freshUpdates, _ => _.id)
|
||||
store.setForTimeline(instanceName, timelineName, { freshUpdates: freshUpdates })
|
||||
store.setForTimeline(instanceName, timelineName, {freshUpdates: freshUpdates})
|
||||
lazilyProcessFreshUpdates(instanceName, timelineName)
|
||||
}
|
|
@ -6,8 +6,8 @@ export async function insertUsername (realm, username, startIndex, endIndex) {
|
|||
let pre = oldText.substring(0, startIndex)
|
||||
let post = oldText.substring(endIndex)
|
||||
let newText = `${pre}@${username} ${post}`
|
||||
store.setComposeData(realm, { text: newText })
|
||||
store.setForAutosuggest(currentInstance, realm, { autosuggestSearchResults: [] })
|
||||
store.setComposeData(realm, {text: newText})
|
||||
store.setForAutosuggest(currentInstance, realm, {autosuggestSearchResults: []})
|
||||
}
|
||||
|
||||
export async function clickSelectedAutosuggestionUsername (realm) {
|
||||
|
@ -29,8 +29,8 @@ export function insertEmojiAtPosition (realm, emoji, startIndex, endIndex) {
|
|||
let pre = oldText.substring(0, startIndex)
|
||||
let post = oldText.substring(endIndex)
|
||||
let newText = `${pre}:${emoji.shortcode}: ${post}`
|
||||
store.setComposeData(realm, { text: newText })
|
||||
store.setForAutosuggest(currentInstance, realm, { autosuggestSearchResults: [] })
|
||||
store.setComposeData(realm, {text: newText})
|
||||
store.setForAutosuggest(currentInstance, realm, {autosuggestSearchResults: []})
|
||||
}
|
||||
|
||||
export async function clickSelectedAutosuggestionEmoji (realm) {
|
|
@ -1,19 +1,18 @@
|
|||
import { store } from '../_store/store'
|
||||
import { blockAccount, unblockAccount } from '../_api/block'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { updateLocalRelationship } from './accounts'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { updateProfileAndRelationship } from './accounts'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
|
||||
export async function setAccountBlocked (accountId, block, toastOnSuccess) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
try {
|
||||
let relationship
|
||||
if (block) {
|
||||
relationship = await blockAccount(currentInstance, accessToken, accountId)
|
||||
await blockAccount(currentInstance, accessToken, accountId)
|
||||
} else {
|
||||
relationship = await unblockAccount(currentInstance, accessToken, accountId)
|
||||
await unblockAccount(currentInstance, accessToken, accountId)
|
||||
}
|
||||
await updateLocalRelationship(currentInstance, accountId, relationship)
|
||||
await updateProfileAndRelationship(accountId)
|
||||
if (toastOnSuccess) {
|
||||
if (block) {
|
||||
toast.say('Blocked account')
|
|
@ -1,14 +1,14 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { postStatus as postStatusToServer } from '../_api/statuses'
|
||||
import { addStatusOrNotification } from './addStatusOrNotification'
|
||||
import { database } from '../_database/database'
|
||||
import { getStatus as getStatusFromDatabase } from '../_database/timelines/getStatusOrNotification'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
import { putMediaDescription } from '../_api/media'
|
||||
|
||||
export async function insertHandleForReply (statusId) {
|
||||
let { currentInstance } = store.get()
|
||||
let status = await database.getStatus(currentInstance, statusId)
|
||||
let status = await getStatusFromDatabase(currentInstance, statusId)
|
||||
let { currentVerifyCredentials } = store.get()
|
||||
let originalStatus = status.reblog || status
|
||||
let accounts = [originalStatus.account].concat(originalStatus.mentions || [])
|
||||
|
@ -22,7 +22,7 @@ export async function insertHandleForReply (statusId) {
|
|||
|
||||
export async function postStatus (realm, text, inReplyToId, mediaIds,
|
||||
sensitive, spoilerText, visibility,
|
||||
mediaDescriptions, inReplyToUuid) {
|
||||
mediaDescriptions = [], inReplyToUuid) {
|
||||
let { currentInstance, accessToken, online } = store.get()
|
||||
|
||||
if (!online) {
|
||||
|
@ -30,9 +30,6 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
|||
return
|
||||
}
|
||||
|
||||
text = text || ''
|
||||
mediaDescriptions = mediaDescriptions || []
|
||||
|
||||
store.set({
|
||||
postingStatus: true
|
||||
})
|
||||
|
@ -49,7 +46,7 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
|||
console.error(e)
|
||||
toast.say('Unable to post status: ' + (e.message || ''))
|
||||
} 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]
|
||||
? replyVisibility
|
||||
: defaultVisibility
|
||||
store.setComposeData(realm, { postPrivacy: visibility })
|
||||
store.setComposeData(realm, {postPrivacy: visibility})
|
||||
}
|
|
@ -1,13 +1,11 @@
|
|||
import { store } from '../_store/store'
|
||||
import { deleteStatus } from '../_api/delete'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { deleteStatus as deleteStatusLocally } from './deleteStatuses'
|
||||
import { toast } from '../_utils/toast'
|
||||
|
||||
export async function doDeleteStatus (statusId) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
try {
|
||||
await deleteStatus(currentInstance, accessToken, statusId)
|
||||
deleteStatusLocally(currentInstance, statusId)
|
||||
toast.say('Status deleted.')
|
||||
} catch (e) {
|
||||
console.error(e)
|
|
@ -1,8 +1,10 @@
|
|||
import { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses'
|
||||
import { store } from '../_store/store'
|
||||
import isEqual from 'lodash-es/isEqual'
|
||||
import { database } from '../_database/database'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
import isEqual from 'lodash-es/isEqual'
|
||||
import {
|
||||
deleteStatusesAndNotifications as deleteStatusesAndNotificationsFromDatabase
|
||||
} from '../_database/timelines/deletion'
|
||||
|
||||
function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) {
|
||||
let keys = ['timelineItemIds', 'itemIdsToAdd']
|
||||
|
@ -16,7 +18,6 @@ function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) {
|
|||
}
|
||||
let filteredIds = ids.filter(idFilter)
|
||||
if (!isEqual(ids, filteredIds)) {
|
||||
console.log('deleting an item from timelineName', timelineName, 'for key', key)
|
||||
store.setForTimeline(instanceName, timelineName, {
|
||||
[key]: filteredIds
|
||||
})
|
||||
|
@ -44,7 +45,7 @@ function deleteNotificationIdsFromStore (instanceName, idsToDelete) {
|
|||
async function deleteStatusesAndNotifications (instanceName, statusIdsToDelete, notificationIdsToDelete) {
|
||||
deleteStatusIdsFromStore(instanceName, statusIdsToDelete)
|
||||
deleteNotificationIdsFromStore(instanceName, notificationIdsToDelete)
|
||||
await database.deleteStatusesAndNotifications(instanceName, statusIdsToDelete, notificationIdsToDelete)
|
||||
await deleteStatusesAndNotificationsFromDatabase(instanceName, statusIdsToDelete, notificationIdsToDelete)
|
||||
}
|
||||
|
||||
async function doDeleteStatus (instanceName, statusId) {
|
|
@ -1,28 +1,30 @@
|
|||
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 { store } from '../_store/store'
|
||||
|
||||
export async function updateCustomEmojiForInstance (instanceName) {
|
||||
await cacheFirstUpdateAfter(
|
||||
() => getCustomEmoji(instanceName),
|
||||
() => database.getCustomEmoji(instanceName),
|
||||
emoji => database.setCustomEmoji(instanceName, emoji),
|
||||
() => getCustomEmojiFromDatabase(instanceName),
|
||||
emoji => setCustomEmojiInDatabase(instanceName, emoji),
|
||||
emoji => {
|
||||
let { customEmoji } = store.get()
|
||||
customEmoji[instanceName] = emoji
|
||||
store.set({ customEmoji: customEmoji })
|
||||
store.set({customEmoji: customEmoji})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function insertEmoji (realm, emoji) {
|
||||
let emojiText = emoji.custom ? emoji.colons : emoji.native
|
||||
let { composeSelectionStart } = store.get()
|
||||
let idx = composeSelectionStart || 0
|
||||
let oldText = store.getComposeData(realm, 'text') || ''
|
||||
let pre = oldText.substring(0, idx)
|
||||
let post = oldText.substring(idx)
|
||||
let newText = `${pre}${emojiText} ${post}`
|
||||
store.setComposeData(realm, { text: newText })
|
||||
let newText = `${pre}:${emoji.shortcode}: ${post}`
|
||||
store.setComposeData(realm, {text: newText})
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
import { favoriteStatus, unfavoriteStatus } from '../_api/favorite'
|
||||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { database } from '../_database/database'
|
||||
import { toast } from '../_utils/toast'
|
||||
import {
|
||||
setStatusFavorited as setStatusFavoritedInDatabase
|
||||
} from '../_database/timelines/updateStatus'
|
||||
|
||||
export async function setFavorited (statusId, favorited) {
|
||||
let { online } = store.get()
|
||||
|
@ -16,7 +18,7 @@ export async function setFavorited (statusId, favorited) {
|
|||
store.setStatusFavorited(currentInstance, statusId, favorited) // optimistic update
|
||||
try {
|
||||
await networkPromise
|
||||
await database.setStatusFavorited(currentInstance, statusId, favorited)
|
||||
await setStatusFavoritedInDatabase(currentInstance, statusId, favorited)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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 { store } from '../_store/store'
|
||||
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { goto } from '../../../__sapper__/client'
|
||||
import { switchToTheme } from '../_utils/themeEngine'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { goto } from 'sapper/runtime.js'
|
||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||
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) {
|
||||
let { instanceThemes } = store.get()
|
||||
instanceThemes[instanceName] = newTheme
|
||||
store.set({ instanceThemes: instanceThemes })
|
||||
store.set({instanceThemes: instanceThemes})
|
||||
store.save()
|
||||
let { currentInstance } = store.get()
|
||||
if (instanceName === currentInstance) {
|
||||
|
@ -55,15 +61,15 @@ export async function logOutOfInstance (instanceName) {
|
|||
})
|
||||
store.save()
|
||||
toast.say(`Logged out of ${instanceName}`)
|
||||
switchToTheme(instanceThemes[newInstance] || DEFAULT_THEME)
|
||||
/* no await */ database.clearDatabaseForInstance(instanceName)
|
||||
switchToTheme(instanceThemes[newInstance] || 'default')
|
||||
await clearDatabaseForInstance(instanceName)
|
||||
goto('/settings/instances')
|
||||
}
|
||||
|
||||
function setStoreVerifyCredentials (instanceName, thisVerifyCredentials) {
|
||||
let { verifyCredentials } = store.get()
|
||||
verifyCredentials[instanceName] = thisVerifyCredentials
|
||||
store.set({ verifyCredentials: verifyCredentials })
|
||||
store.set({verifyCredentials: verifyCredentials})
|
||||
}
|
||||
|
||||
export async function updateVerifyCredentialsForInstance (instanceName) {
|
||||
|
@ -71,8 +77,8 @@ export async function updateVerifyCredentialsForInstance (instanceName) {
|
|||
let accessToken = loggedInInstances[instanceName].access_token
|
||||
await cacheFirstUpdateAfter(
|
||||
() => getVerifyCredentials(instanceName, accessToken),
|
||||
() => database.getInstanceVerifyCredentials(instanceName),
|
||||
verifyCredentials => database.setInstanceVerifyCredentials(instanceName, verifyCredentials),
|
||||
() => getInstanceVerifyCredentialsFromDatabase(instanceName),
|
||||
verifyCredentials => setInstanceVerifyCredentialsInDatabase(instanceName, verifyCredentials),
|
||||
verifyCredentials => setStoreVerifyCredentials(instanceName, verifyCredentials)
|
||||
)
|
||||
}
|
||||
|
@ -85,12 +91,12 @@ export async function updateVerifyCredentialsForCurrentInstance () {
|
|||
export async function updateInstanceInfo (instanceName) {
|
||||
await cacheFirstUpdateAfter(
|
||||
() => getInstanceInfo(instanceName),
|
||||
() => database.getInstanceInfo(instanceName),
|
||||
info => database.setInstanceInfo(instanceName, info),
|
||||
() => getInstanceInfoFromDatabase(instanceName),
|
||||
info => setInstanceInfoInDatabase(instanceName, info),
|
||||
info => {
|
||||
let { instanceInfos } = store.get()
|
||||
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 { uploadMedia } from '../_api/media'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||
|
||||
export async function doMediaUpload (realm, file) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
store.set({ uploadingMedia: true })
|
||||
store.set({uploadingMedia: true})
|
||||
try {
|
||||
let response = await uploadMedia(currentInstance, accessToken, file)
|
||||
let composeMedia = store.getComposeData(realm, 'media') || []
|
||||
if (composeMedia.length === 4) {
|
||||
throw new Error('Only 4 media max are allowed')
|
||||
}
|
||||
composeMedia.push({
|
||||
data: response,
|
||||
file: { name: file.name },
|
||||
description: ''
|
||||
file: { name: file.name }
|
||||
})
|
||||
let composeText = store.getComposeData(realm, 'text') || ''
|
||||
composeText += ' ' + response.text_url
|
||||
store.setComposeData(realm, {
|
||||
media: composeMedia
|
||||
media: composeMedia,
|
||||
text: composeText
|
||||
})
|
||||
scheduleIdleTask(() => store.save())
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say('Failed to upload media: ' + (e.message || ''))
|
||||
} finally {
|
||||
store.set({ uploadingMedia: false })
|
||||
store.set({uploadingMedia: false})
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteMedia (realm, i) {
|
||||
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, {
|
||||
media: composeMedia
|
||||
media: composeMedia,
|
||||
text: composeText,
|
||||
mediaDescriptions: mediaDescriptions
|
||||
})
|
||||
scheduleIdleTask(() => store.save())
|
||||
}
|
|
@ -1,19 +1,18 @@
|
|||
import { store } from '../_store/store'
|
||||
import { muteAccount, unmuteAccount } from '../_api/mute'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { updateLocalRelationship } from './accounts'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { updateProfileAndRelationship } from './accounts'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
|
||||
export async function setAccountMuted (accountId, mute, toastOnSuccess) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
try {
|
||||
let relationship
|
||||
if (mute) {
|
||||
relationship = await muteAccount(currentInstance, accessToken, accountId)
|
||||
await muteAccount(currentInstance, accessToken, accountId)
|
||||
} else {
|
||||
relationship = await unmuteAccount(currentInstance, accessToken, accountId)
|
||||
await unmuteAccount(currentInstance, accessToken, accountId)
|
||||
}
|
||||
await updateLocalRelationship(currentInstance, accountId, relationship)
|
||||
await updateProfileAndRelationship(accountId)
|
||||
if (toastOnSuccess) {
|
||||
if (mute) {
|
||||
toast.say('Muted account')
|
|
@ -1,7 +1,7 @@
|
|||
import { store } from '../_store/store'
|
||||
import { muteConversation, unmuteConversation } from '../_api/muteConversation'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { database } from '../_database/database'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { setStatusMuted as setStatusMutedInDatabase } from '../_database/timelines/updateStatus'
|
||||
|
||||
export async function setConversationMuted (statusId, mute, toastOnSuccess) {
|
||||
let { currentInstance, accessToken } = store.get()
|
||||
|
@ -11,7 +11,7 @@ export async function setConversationMuted (statusId, mute, toastOnSuccess) {
|
|||
} else {
|
||||
await unmuteConversation(currentInstance, accessToken, statusId)
|
||||
}
|
||||
await database.setStatusMuted(currentInstance, statusId, mute)
|
||||
await setStatusMutedInDatabase(currentInstance, statusId, mute)
|
||||
if (toastOnSuccess) {
|
||||
if (mute) {
|
||||
toast.say('Muted conversation')
|
|
@ -1,7 +1,7 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { pinStatus, unpinStatus } from '../_api/pin'
|
||||
import { database } from '../_database/database'
|
||||
import { setStatusPinned as setStatusPinnedInDatabase } from '../_database/timelines/updateStatus'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
|
||||
export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSuccess) {
|
||||
|
@ -19,8 +19,7 @@ export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSucces
|
|||
toast.say('Unpinned status')
|
||||
}
|
||||
}
|
||||
store.setStatusPinned(currentInstance, statusId, pinned)
|
||||
await database.setStatusPinned(currentInstance, statusId, pinned)
|
||||
await setStatusPinnedInDatabase(currentInstance, statusId, pinned)
|
||||
emit('updatePinnedStatuses')
|
||||
} catch (e) {
|
||||
console.error(e)
|
|
@ -1,6 +1,9 @@
|
|||
import { store } from '../_store/store'
|
||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||
import { database } from '../_database/database'
|
||||
import {
|
||||
getPinnedStatuses as getPinnedStatusesFromDatabase,
|
||||
insertPinnedStatuses as insertPinnedStatusesInDatabase
|
||||
} from '../_database/timelines/pinnedStatuses'
|
||||
import {
|
||||
getPinnedStatuses
|
||||
} from '../_api/pinnedStatuses'
|
||||
|
@ -10,13 +13,13 @@ export async function updatePinnedStatusesForAccount (accountId) {
|
|||
|
||||
await cacheFirstUpdateAfter(
|
||||
() => getPinnedStatuses(currentInstance, accessToken, accountId),
|
||||
() => database.getPinnedStatuses(currentInstance, accountId),
|
||||
statuses => database.insertPinnedStatuses(currentInstance, accountId, statuses),
|
||||
() => getPinnedStatusesFromDatabase(currentInstance, accountId),
|
||||
statuses => insertPinnedStatusesInDatabase(currentInstance, accountId, statuses),
|
||||
statuses => {
|
||||
let { pinnedStatuses } = store.get()
|
||||
pinnedStatuses[currentInstance] = pinnedStatuses[currentInstance] || {}
|
||||
pinnedStatuses[currentInstance][accountId] = statuses
|
||||
store.set({ pinnedStatuses: pinnedStatuses })
|
||||
store.set({pinnedStatuses: pinnedStatuses})
|
||||
}
|
||||
)
|
||||
}
|
|
@ -2,5 +2,5 @@
|
|||
import { store } from '../_store/store'
|
||||
|
||||
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 { toast } from '../_components/toast/toast'
|
||||
import { toast } from '../_utils/toast'
|
||||
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) {
|
||||
let online = store.get()
|
||||
|
@ -16,7 +16,7 @@ export async function setReblogged (statusId, reblogged) {
|
|||
store.setStatusReblogged(currentInstance, statusId, reblogged) // optimistic update
|
||||
try {
|
||||
await networkPromise
|
||||
await database.setStatusReblogged(currentInstance, statusId, reblogged)
|
||||
await setStatusRebloggedInDatabase(currentInstance, statusId, reblogged)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.say(`Failed to ${reblogged ? 'boost' : 'unboost'}. ` + (e.message || ''))
|
|
@ -1,7 +1,7 @@
|
|||
import { store } from '../_store/store'
|
||||
import { approveFollowRequest, rejectFollowRequest } from '../_api/requests'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { toast } from '../_utils/toast'
|
||||
|
||||
export async function setFollowRequestApprovedOrRejected (accountId, approved, toastOnSuccess) {
|
||||
let {
|
|
@ -1,10 +1,10 @@
|
|||
import { store } from '../_store/store'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { search } from '../_api/search'
|
||||
|
||||
export async function doSearch () {
|
||||
let { currentInstance, accessToken, queryInSearch } = store.get()
|
||||
store.set({ searchLoading: true })
|
||||
store.set({searchLoading: true})
|
||||
try {
|
||||
let results = await search(currentInstance, accessToken, queryInSearch)
|
||||
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 || ''))
|
||||
console.error(e)
|
||||
} 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) {
|
||||
let status = await database.getStatus(instanceName, statusId)
|
||||
let status = await getStatusFromDatabase(instanceName, statusId)
|
||||
return status.reblog && status.reblog.id
|
||||
}
|
||||
|
||||
|
@ -13,9 +19,9 @@ export async function getIdsThatTheseStatusesReblogged (instanceName, statusIds)
|
|||
}
|
||||
|
||||
export async function getIdsThatRebloggedThisStatus (instanceName, statusId) {
|
||||
return database.getReblogsForStatus(instanceName, statusId)
|
||||
return getReblogsForStatusFromDatabase(instanceName, statusId)
|
||||
}
|
||||
|
||||
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 { getTimeline } from '../_api/timelines'
|
||||
import { toast } from '../_components/toast/toast'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { mark, stop } from '../_utils/marks'
|
||||
import { concat, mergeArrays } from '../_utils/arrays'
|
||||
import { mergeArrays } from '../_utils/arrays'
|
||||
import { byItemIds } from '../_utils/sorting'
|
||||
import isEqual from 'lodash-es/isEqual'
|
||||
import { database } from '../_database/database'
|
||||
import { getStatus, getStatusContext } from '../_api/statuses'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
import { TIMELINE_BATCH_SIZE } from '../_static/timelines'
|
||||
import {
|
||||
insertTimelineItems as insertTimelineItemsInDatabase
|
||||
} from '../_database/timelines/insertion'
|
||||
import {
|
||||
getTimeline as getTimelineFromDatabase
|
||||
} from '../_database/timelines/pagination'
|
||||
|
||||
async function storeFreshTimelineItemsInDatabase (instanceName, timelineName, items) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
const FETCH_LIMIT = 20
|
||||
|
||||
async function fetchTimelineItems (instanceName, accessToken, timelineName, lastTimelineItemId, online) {
|
||||
mark('fetchTimelineItems')
|
||||
let items
|
||||
let stale = false
|
||||
if (!online) {
|
||||
items = await database.getTimeline(instanceName, timelineName, lastTimelineItemId, TIMELINE_BATCH_SIZE)
|
||||
items = await getTimelineFromDatabase(instanceName, timelineName, lastTimelineItemId, FETCH_LIMIT)
|
||||
stale = true
|
||||
} else {
|
||||
try {
|
||||
console.log('fetchTimelineItemsFromNetwork')
|
||||
items = await fetchTimelineItemsFromNetwork(instanceName, accessToken, timelineName, lastTimelineItemId)
|
||||
/* no await */ storeFreshTimelineItemsInDatabase(instanceName, timelineName, items)
|
||||
items = await getTimeline(instanceName, accessToken, timelineName, lastTimelineItemId, FETCH_LIMIT)
|
||||
/* no await */ insertTimelineItemsInDatabase(instanceName, timelineName, items)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -72,10 +51,10 @@ export async function addTimelineItemIds (instanceName, timelineName, newIds, ne
|
|||
let mergedIds = mergeArrays(oldIds || [], newIds)
|
||||
|
||||
if (!isEqual(oldIds, mergedIds)) {
|
||||
store.setForTimeline(instanceName, timelineName, { timelineItemIds: mergedIds })
|
||||
store.setForTimeline(instanceName, timelineName, {timelineItemIds: mergedIds})
|
||||
}
|
||||
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) {
|
||||
console.log('setting runningUpdate: true')
|
||||
store.setForTimeline(instanceName, timelineName, { runningUpdate: true })
|
||||
await fetchTimelineItemsAndPossiblyFallBack()
|
||||
console.log('setting 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 timelineItemIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds')
|
||||
// TODO: update database and do the thread merge correctly
|
||||
for (let itemIdToAdd of itemIdsToAdd) {
|
||||
if (!timelineItemIds.includes(itemIdToAdd)) {
|
||||
timelineItemIds.push(itemIdToAdd)
|
||||
}
|
||||
}
|
||||
timelineItemIds = timelineItemIds.concat(itemIdsToAdd)
|
||||
store.setForTimeline(instanceName, timelineName, {
|
||||
itemIdsToAdd: [],
|
||||
timelineItemIds: timelineItemIds
|
|
@ -3,10 +3,10 @@ import { post, WRITE_TIMEOUT } from '../_utils/ajax'
|
|||
|
||||
export async function blockAccount (instanceName, accessToken, accountId) {
|
||||
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) {
|
||||
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) {
|
||||
let url = `${basename(instanceName)}/api/v1/blocks`
|
||||
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) {
|
||||
let url = `${basename(instanceName)}/api/v1/mutes`
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/following`
|
||||
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) {
|
||||
let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/followers`
|
||||
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) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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'
|
||||
|
||||
const WEBSITE = 'https://pinafore.social'
|
||||
const SCOPES = 'read write follow push'
|
||||
const SCOPES = 'read write follow'
|
||||
const CLIENT_NAME = 'Pinafore'
|
||||
|
||||
export function registerApplication (instanceName, redirectUri) {
|
||||
|
@ -12,7 +12,7 @@ export function registerApplication (instanceName, redirectUri) {
|
|||
redirect_uris: redirectUri,
|
||||
scopes: SCOPES,
|
||||
website: WEBSITE
|
||||
}, null, { timeout: WRITE_TIMEOUT })
|
||||
}, null, {timeout: WRITE_TIMEOUT})
|
||||
}
|
||||
|
||||
export function generateAuthLink (instanceName, clientId, redirectUri) {
|
||||
|
@ -33,5 +33,5 @@ export function getAccessTokenFromAuthCode (instanceName, clientId, clientSecret
|
|||
redirect_uri: redirectUri,
|
||||
grant_type: 'authorization_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) {
|
||||
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) {
|
||||
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,
|
||||
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) {
|
||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/reblogged_by`
|
||||
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) {
|
||||
let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/favourited_by`
|
||||
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) {
|
||||
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) {
|
||||
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,
|
||||
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/')) {
|
||||
return 'timelines/tag'
|
||||
} else if (timeline.startsWith('status/')) {
|
||||
return 'statuses'
|
||||
} else if (timeline.startsWith('account/')) {
|
||||
return 'accounts'
|
||||
} 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 url = `${basename(instanceName)}/api/v1/${timelineUrlName}`
|
||||
|
||||
if (timeline.startsWith('tag/')) {
|
||||
url += '/' + timeline.split('/').slice(-1)[0]
|
||||
} else if (timeline.startsWith('status/')) {
|
||||
url += '/' + timeline.split('/').slice(-1)[0] + '/context'
|
||||
} else if (timeline.startsWith('account/')) {
|
||||
url += '/' + timeline.split('/').slice(-1)[0] + '/statuses'
|
||||
} else if (timeline.startsWith('list/')) {
|
||||
|
@ -43,15 +47,22 @@ export function getTimeline (instanceName, accessToken, timeline, maxId, since,
|
|||
params.max_id = maxId
|
||||
}
|
||||
|
||||
if (limit) {
|
||||
params.limit = limit
|
||||
}
|
||||
|
||||
if (timeline === 'local') {
|
||||
params.local = true
|
||||
}
|
||||
|
||||
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) {
|
||||
return instanceName.startsWith('localhost:') || instanceName.startsWith('127.0.0.1:')
|
||||
}
|
||||
|
||||
export function basename (instanceName) {
|
||||
if (targetIsLocalhost(instanceName)) {
|
||||
if (isLocalhost && targetIsLocalhost(instanceName)) {
|
||||
return `http://${instanceName}`
|
||||
}
|
||||
return `https://${instanceName}`
|
|
@ -32,9 +32,9 @@
|
|||
</style>
|
||||
<script>
|
||||
import { store } from '../_store/store'
|
||||
import LoadingPage from './LoadingPage.html'
|
||||
import AccountSearchResult from './search/AccountSearchResult.html'
|
||||
import { toast } from './toast/toast'
|
||||
import LoadingPage from '../_components/LoadingPage.html'
|
||||
import AccountSearchResult from '../_components/search/AccountSearchResult.html'
|
||||
import { toast } from '../_utils/toast'
|
||||
import { on } from '../_utils/eventBus'
|
||||
|
||||
// TODO: paginate
|
||||
|
@ -45,7 +45,7 @@
|
|||
} catch (e) {
|
||||
toast.say('Error: ' + (e.name || '') + ' ' + (e.message || ''))
|
||||
} finally {
|
||||
this.set({ loading: false })
|
||||
this.set({loading: false})
|
||||
}
|
||||
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}
|
||||
<svg class={computedClass} style={svgStyle} aria-hidden="true">
|
||||
<svg class={computedClass} aria-hidden="true">
|
||||
<use xlink:href="#fa-user" />
|
||||
</svg>
|
||||
{:elseif $autoplayGifs}
|
||||
<LazyImage
|
||||
className={computedClass}
|
||||
ariaHidden="true"
|
||||
forceSize=true
|
||||
<img
|
||||
class={computedClass}
|
||||
aria-hidden="true"
|
||||
alt=""
|
||||
src={account.avatar}
|
||||
{width}
|
||||
{height}
|
||||
on:imgLoad="set({loaded: true})"
|
||||
on:imgLoadError="set({error: true})" />
|
||||
{:else}
|
||||
|
@ -20,8 +17,6 @@
|
|||
alt=""
|
||||
src={account.avatar}
|
||||
staticSrc={account.avatar_static}
|
||||
{width}
|
||||
{height}
|
||||
{isLink}
|
||||
on:imgLoad="set({loaded: true})"
|
||||
on:imgLoadError="set({error: true})"
|
||||
|
@ -37,17 +32,48 @@
|
|||
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 {
|
||||
fill: var(--deemphasized-text-color);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import { imgLoad, imgLoadError } from '../_utils/events'
|
||||
import { store } from '../_store/store'
|
||||
import NonAutoplayImg from './NonAutoplayImg.html'
|
||||
import { classname } from '../_utils/classname'
|
||||
import LazyImage from './LazyImage.html'
|
||||
|
||||
export default {
|
||||
events: {
|
||||
imgLoad,
|
||||
imgLoadError
|
||||
},
|
||||
data: () => ({
|
||||
className: void 0,
|
||||
loaded: false,
|
||||
|
@ -56,30 +82,15 @@
|
|||
}),
|
||||
store: () => store,
|
||||
computed: {
|
||||
computedClass: ({ className, loaded }) => (classname(
|
||||
computedClass: ({ className, loaded, size }) => (classname(
|
||||
'avatar',
|
||||
className,
|
||||
loaded && 'loaded'
|
||||
)),
|
||||
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;`
|
||||
loaded && 'loaded',
|
||||
`size-${size}`
|
||||
))
|
||||
},
|
||||
components: {
|
||||
NonAutoplayImg,
|
||||
LazyImage
|
||||
NonAutoplayImg
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|