-
+
{children}
diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js
index 0a4f44405..fb98c4f5b 100644
--- a/app/soapbox/features/ui/util/async-components.js
+++ b/app/soapbox/features/ui/util/async-components.js
@@ -94,6 +94,10 @@ export function Reblogs() {
return import(/* webpackChunkName: "features/reblogs" */'../../reblogs');
}
+export function Reactions() {
+ return import(/* webpackChunkName: "features/reactions" */'../../reactions');
+}
+
export function Favourites() {
return import(/* webpackChunkName: "features/favourites" */'../../favourites');
}
diff --git a/app/soapbox/middleware/sounds.js b/app/soapbox/middleware/sounds.js
index a2fc7572f..6950e7618 100644
--- a/app/soapbox/middleware/sounds.js
+++ b/app/soapbox/middleware/sounds.js
@@ -1,8 +1,5 @@
'use strict';
-import { join } from 'path';
-import { FE_SUBDIRECTORY } from 'soapbox/build_config';
-
const createAudio = sources => {
const audio = new Audio();
sources.forEach(({ type, src }) => {
@@ -31,21 +28,21 @@ export default function soundsMiddleware() {
const soundCache = {
boop: createAudio([
{
- src: join(FE_SUBDIRECTORY, '/sounds/boop.ogg'),
+ src: require('../../sounds/boop.ogg'),
type: 'audio/ogg',
},
{
- src: join(FE_SUBDIRECTORY, '/sounds/boop.mp3'),
+ src: require('../../sounds/boop.mp3'),
type: 'audio/mpeg',
},
]),
chat: createAudio([
{
- src: join(FE_SUBDIRECTORY, '/sounds/chat.oga'),
+ src: require('../../sounds/chat.oga'),
type: 'audio/ogg',
},
{
- src: join(FE_SUBDIRECTORY, '/sounds/chat.mp3'),
+ src: require('../../sounds/chat.mp3'),
type: 'audio/mpeg',
},
]),
diff --git a/app/soapbox/reducers/__tests__/user_lists-test.js b/app/soapbox/reducers/__tests__/user_lists-test.js
index feaaca3e6..7d571e208 100644
--- a/app/soapbox/reducers/__tests__/user_lists-test.js
+++ b/app/soapbox/reducers/__tests__/user_lists-test.js
@@ -10,6 +10,7 @@ describe('user_lists reducer', () => {
favourited_by: ImmutableMap(),
follow_requests: ImmutableMap(),
blocks: ImmutableMap(),
+ reactions: ImmutableMap(),
mutes: ImmutableMap(),
groups: ImmutableMap(),
groups_removed_accounts: ImmutableMap(),
diff --git a/app/soapbox/reducers/user_lists.js b/app/soapbox/reducers/user_lists.js
index 9afaf55b9..f0d80de77 100644
--- a/app/soapbox/reducers/user_lists.js
+++ b/app/soapbox/reducers/user_lists.js
@@ -14,6 +14,7 @@ import {
import {
REBLOGS_FETCH_SUCCESS,
FAVOURITES_FETCH_SUCCESS,
+ REACTIONS_FETCH_SUCCESS,
} from '../actions/interactions';
import {
BLOCKS_FETCH_SUCCESS,
@@ -37,6 +38,7 @@ const initialState = ImmutableMap({
following: ImmutableMap(),
reblogged_by: ImmutableMap(),
favourited_by: ImmutableMap(),
+ reactions: ImmutableMap(),
follow_requests: ImmutableMap(),
blocks: ImmutableMap(),
mutes: ImmutableMap(),
@@ -77,6 +79,8 @@ export default function userLists(state = initialState, action) {
return state.setIn(['reblogged_by', action.id], ImmutableOrderedSet(action.accounts.map(item => item.id)));
case FAVOURITES_FETCH_SUCCESS:
return state.setIn(['favourited_by', action.id], ImmutableOrderedSet(action.accounts.map(item => item.id)));
+ case REACTIONS_FETCH_SUCCESS:
+ return state.setIn(['reactions', action.id], action.reactions.map(({ accounts, ...reaction }) => ({ ...reaction, accounts: ImmutableOrderedSet(accounts.map(account => account.id)) })));
case NOTIFICATIONS_UPDATE:
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
case FOLLOW_REQUESTS_FETCH_SUCCESS:
diff --git a/app/soapbox/utils/features.js b/app/soapbox/utils/features.js
index 518f3e669..86a02479a 100644
--- a/app/soapbox/utils/features.js
+++ b/app/soapbox/utils/features.js
@@ -25,6 +25,7 @@ export const getFeatures = createSelector([
settingsStore: v.software === 'Pleroma',
accountAliasesAPI: v.software === 'Pleroma',
resetPasswordAPI: v.software === 'Pleroma',
+ exposableReactions: features.includes('exposable_reactions'),
};
});
diff --git a/app/styles/accounts.scss b/app/styles/accounts.scss
index ad2190be5..14f74f59a 100644
--- a/app/styles/accounts.scss
+++ b/app/styles/accounts.scss
@@ -216,6 +216,13 @@
.account__avatar-wrapper {
float: left;
margin-right: 12px;
+
+ .emoji-react__emoji {
+ position: absolute;
+ top: 36px;
+ left: 32px;
+ z-index: 1;
+ }
}
.account__avatar {
diff --git a/app/styles/components/emoji-reacts.scss b/app/styles/components/emoji-reacts.scss
index 4fca2108c..bc69b0542 100644
--- a/app/styles/components/emoji-reacts.scss
+++ b/app/styles/components/emoji-reacts.scss
@@ -1,6 +1,8 @@
.emoji-react {
display: inline-block;
transition: 0.1s;
+ color: var(--primary-text-color--faint);
+ text-decoration: none;
&__emoji {
img {
@@ -20,8 +22,6 @@
}
.emoji-react--reblogs {
- color: var(--primary-text-color--faint);
- text-decoration: none;
vertical-align: middle;
display: inline-flex;
diff --git a/app/styles/ui.scss b/app/styles/ui.scss
index d710bc180..8a111f31e 100644
--- a/app/styles/ui.scss
+++ b/app/styles/ui.scss
@@ -613,7 +613,8 @@
.notification__filter-bar,
.search__filter-bar,
-.account__section-headline {
+.account__section-headline,
+.reaction__filter-bar {
border-bottom: 1px solid var(--brand-color--faint);
cursor: default;
display: flex;
@@ -663,6 +664,17 @@
}
}
+.reaction__filter-bar {
+ overflow-x: auto;
+ overflow-y: hidden;
+
+ a {
+ flex: unset;
+ padding: 15px 24px;
+ min-width: max-content;
+ }
+}
+
::-webkit-scrollbar-thumb {
border-radius: 0;
}
diff --git a/jest.config.js b/jest.config.js
index d7b2fb44a..efe164457 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -31,4 +31,11 @@ module.exports = {
'/app',
],
'testEnvironment': 'jsdom',
+ 'moduleNameMapper': {
+ '^.+.(css|styl|less|sass|scss|png|jpg|svg|ttf|woff|woff2)$': 'jest-transform-stub',
+ },
+ 'transform': {
+ '\\.[jt]sx?$': 'babel-jest',
+ '.+\\.(css|styl|less|sass|scss|png|jpg|svg|ttf|woff|woff2)$': 'jest-transform-stub',
+ },
};
diff --git a/package.json b/package.json
index 018088f2d..419294299 100644
--- a/package.json
+++ b/package.json
@@ -92,6 +92,7 @@
"intl-messageformat-parser": "^6.0.0",
"intl-pluralrules": "^1.3.0",
"is-nan": "^1.2.1",
+ "jest-transform-stub": "^2.0.0",
"jsdoc": "~3.6.7",
"lodash": "^4.7.11",
"mark-loader": "^0.1.6",
diff --git a/webpack/configuration.js b/webpack/configuration.js
index 0089469c4..960e25e60 100644
--- a/webpack/configuration.js
+++ b/webpack/configuration.js
@@ -12,7 +12,6 @@ const settings = {
test_root_path: `${FE_BUILD_DIR}-test`,
cache_path: 'tmp/cache',
resolved_paths: [],
- static_assets_extensions: [ '.jpg', '.jpeg', '.png', '.tiff', '.ico', '.svg', '.gif', '.eot', '.otf', '.ttf', '.woff', '.woff2', '.mp3', '.ogg', '.oga' ],
extensions: [ '.mjs', '.js', '.sass', '.scss', '.css', '.module.sass', '.module.scss', '.module.css', '.png', '.svg', '.gif', '.jpeg', '.jpg' ],
};
diff --git a/webpack/production.js b/webpack/production.js
index 9968a2ca9..cccfc4163 100644
--- a/webpack/production.js
+++ b/webpack/production.js
@@ -25,37 +25,33 @@ module.exports = merge(sharedConfig, {
new OfflinePlugin({
caches: {
main: [':rest:'],
- additional: [':externals:'],
+ additional: [
+ 'packs/emoji/1f602-*.svg', // used for emoji picker dropdown
+ 'packs/images/32-*.png', // used in emoji-mart
+
+ // Default emoji reacts
+ 'packs/emoji/1f44d-*.svg', // Thumbs up
+ 'packs/emoji/2764-*.svg', // Heart
+ 'packs/emoji/1f606-*.svg', // Laughing
+ 'packs/emoji/1f62e-*.svg', // Surprised
+ 'packs/emoji/1f622-*.svg', // Crying
+ 'packs/emoji/1f629-*.svg', // Weary
+ 'packs/emoji/1f621-*.svg', // Angry (Spinster)
+ ],
optional: [
'**/locale_*.js', // don't fetch every locale; the user only needs one
'**/*_polyfills-*.js', // the user may not need polyfills
'**/*.chunk.js', // only cache chunks when needed
'**/*.woff2', // the user may have system-fonts enabled
- // images/audio can be cached on-demand
+ // images can be cached on-demand
'**/*.png',
- '**/*.jpg',
- '**/*.jpeg',
'**/*.svg',
- '**/*.mp3',
- '**/*.ogg',
],
},
- externals: [
- '/emoji/1f602.svg', // used for emoji picker dropdown
- '/emoji/sheet_13.png', // used in emoji-mart
-
- // Default emoji reacts
- '/emoji/1f44d.svg', // Thumbs up
- '/emoji/2764.svg', // Heart
- '/emoji/1f606.svg', // Laughing
- '/emoji/1f62e.svg', // Surprised
- '/emoji/1f622.svg', // Crying
- '/emoji/1f629.svg', // Weary
- '/emoji/1f621.svg', // Angry (Spinster)
- ],
excludes: [
'**/*.gz',
'**/*.map',
+ '**/*.LICENSE.txt',
'stats.json',
'report.html',
'instance/**/*',
@@ -66,15 +62,20 @@ module.exports = merge(sharedConfig, {
'**/*.woff',
// Sounds return a 206 causing sw.js to crash
// https://stackoverflow.com/a/66335638
- 'sounds/**/*',
- // Don't cache index.html
+ '**/*.ogg',
+ '**/*.oga',
+ '**/*.mp3',
+ // Don't serve index.html
+ // https://github.com/bromite/bromite/issues/1294
'index.html',
+ '404.html',
],
// ServiceWorker: {
// entry: join(__dirname, '../app/soapbox/service_worker/entry.js'),
// cacheName: 'soapbox',
// minify: true,
// },
+ safeToUseOptionalCaches: true,
}),
],
});
diff --git a/webpack/rules/assets.js b/webpack/rules/assets.js
new file mode 100644
index 000000000..2c6fb3f0d
--- /dev/null
+++ b/webpack/rules/assets.js
@@ -0,0 +1,50 @@
+// Asset modules
+// https://webpack.js.org/guides/asset-modules/
+
+const { resolve } = require('path');
+
+// These are processed in reverse-order
+// We use the name 'packs' instead of 'assets' for legacy reasons
+module.exports = [{
+ test: /\.(png|svg)/,
+ type: 'asset/resource',
+ include: [
+ resolve('app', 'images'),
+ resolve('node_modules', 'emoji-datasource'),
+ ],
+ generator: {
+ filename: 'packs/images/[name]-[contenthash:8][ext]',
+ },
+}, {
+ test: /\.(ttf|eot|svg|woff|woff2)/,
+ type: 'asset/resource',
+ include: [
+ resolve('app', 'fonts'),
+ resolve('node_modules', 'fork-awesome'),
+ resolve('node_modules', '@fontsource'),
+ ],
+ generator: {
+ filename: 'packs/fonts/[name]-[contenthash:8][ext]',
+ },
+}, {
+ test: /\.(ogg|oga|mp3)/,
+ type: 'asset/resource',
+ include: resolve('app', 'sounds'),
+ generator: {
+ filename: 'packs/sounds/[name]-[contenthash:8][ext]',
+ },
+}, {
+ test: /\.svg$/,
+ type: 'asset/resource',
+ include: resolve('node_modules', 'twemoji'),
+ generator: {
+ filename: 'packs/emoji/[name]-[contenthash:8][ext]',
+ },
+}, {
+ test: /\.svg$/,
+ type: 'asset/resource',
+ include: resolve('node_modules', 'cryptocurrency-icons'),
+ generator: {
+ filename: 'packs/images/crypto/[name]-[contenthash:8][ext]',
+ },
+}];
diff --git a/webpack/rules/file.js b/webpack/rules/file.js
deleted file mode 100644
index d23a0a977..000000000
--- a/webpack/rules/file.js
+++ /dev/null
@@ -1,20 +0,0 @@
-const { join } = require('path');
-const { settings } = require('../configuration');
-
-module.exports = {
- test: new RegExp(`(${settings.static_assets_extensions.join('|')})$`, 'i'),
- use: [
- {
- loader: 'file-loader',
- options: {
- name(file) {
- if (file.includes(settings.source_path)) {
- return 'packs/media/[path][name]-[contenthash].[ext]';
- }
- return 'packs/media/[folder]/[name]-[contenthash:8].[ext]';
- },
- context: join(settings.source_path),
- },
- },
- ],
-};
diff --git a/webpack/rules/index.js b/webpack/rules/index.js
index 91a4abd19..d3290659e 100644
--- a/webpack/rules/index.js
+++ b/webpack/rules/index.js
@@ -3,14 +3,14 @@ const git = require('./babel-git');
const gitRefresh = require('./git-refresh');
const buildConfig = require('./babel-build-config');
const css = require('./css');
-const file = require('./file');
+const assets = require('./assets');
const nodeModules = require('./node_modules');
// Webpack loaders are processed in reverse order
// https://webpack.js.org/concepts/loaders/#loader-features
// Lastly, process static files using file loader
module.exports = [
- file,
+ ...assets,
css,
nodeModules,
babel,
diff --git a/webpack/shared.js b/webpack/shared.js
index c0bbad931..0c4bd856e 100644
--- a/webpack/shared.js
+++ b/webpack/shared.js
@@ -30,10 +30,9 @@ const makeHtmlConfig = (params = {}) => {
};
module.exports = {
- entry: Object.assign(
- { application: resolve('app/application.js') },
- { styles: resolve(join(settings.source_path, 'styles/application.scss')) },
- ),
+ entry: {
+ application: resolve('app/application.js'),
+ },
output: {
filename: 'packs/js/[name]-[chunkhash].js',
@@ -65,7 +64,7 @@ module.exports = {
},
module: {
- rules: Object.keys(rules).map(key => rules[key]),
+ rules,
},
plugins: [
@@ -89,15 +88,6 @@ module.exports = {
new HtmlWebpackHarddiskPlugin(),
new CopyPlugin({
patterns: [{
- from: join(__dirname, '../node_modules/twemoji/assets/svg'),
- to: join(output.path, 'emoji'),
- }, {
- from: join(__dirname, '../node_modules/emoji-datasource/img/twitter/sheets/32.png'),
- to: join(output.path, 'emoji/sheet_13.png'),
- }, {
- from: join(__dirname, '../app/sounds'),
- to: join(output.path, 'sounds'),
- }, {
from: join(__dirname, '../app/instance'),
to: join(output.path, 'instance'),
}],
diff --git a/yarn.lock b/yarn.lock
index 4876a41de..9e75304e7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7239,6 +7239,11 @@ jest-snapshot@^27.1.0:
pretty-format "^27.1.0"
semver "^7.3.2"
+jest-transform-stub@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/jest-transform-stub/-/jest-transform-stub-2.0.0.tgz#19018b0851f7568972147a5d60074b55f0225a7d"
+ integrity sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg==
+
jest-util@^27.0.0:
version "27.0.6"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.0.6.tgz#e8e04eec159de2f4d5f57f795df9cdc091e50297"