misskey/src/client/store.ts

444 lines
9.9 KiB
TypeScript
Raw Normal View History

import Vuex from 'vuex';
import createPersistedState from 'vuex-persistedstate';
import * as nestedProperty from 'nested-property';
import { faTerminal, faHashtag, faBroadcastTower, faFireAlt, faSearch, faStar, faAt, faListUl, faUserClock, faUsers, faCloud, faGamepad, faFileAlt, faSatellite, faDoorClosed } from '@fortawesome/free-solid-svg-icons';
import { faBell, faEnvelope, faComments } from '@fortawesome/free-regular-svg-icons';
2020-03-31 09:11:43 +09:00
import { apiUrl } from './config';
export const defaultSettings = {
tutorial: 0,
keepCw: false,
showFullAcct: false,
rememberNoteVisibility: false,
defaultNoteVisibility: 'public',
defaultNoteLocalOnly: false,
uploadFolder: null,
pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
memo: null,
reactions: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
};
export const defaultDeviceUserSettings = {
visibility: 'public',
localOnly: false,
widgets: [],
tl: {
src: 'home'
},
menu: [
'notifications',
'messaging',
'drive',
'-',
'followRequests',
'featured',
'explore',
'announcements',
'search',
],
};
export const defaultDeviceSettings = {
lang: null,
loadRawImages: false,
alwaysShowNsfw: false,
useOsNativeEmojis: false,
2020-02-06 17:21:28 +09:00
autoReload: false,
accounts: [],
recentEmojis: [],
themes: [],
2020-03-22 10:39:12 +09:00
darkTheme: '8c539dc1-0fab-4d47-9194-39c508e9bfe1',
lightTheme: '4eea646f-7afa-4645-83e9-83af0333cd37',
darkMode: false,
syncDeviceDarkMode: true,
2020-02-06 19:11:14 +09:00
animation: true,
animatedMfm: true,
2020-02-16 22:46:18 +09:00
imageNewTab: false,
2020-02-19 03:16:10 +09:00
showFixedPostForm: false,
2020-04-13 23:46:53 +09:00
disablePagesScript: true,
enableInfiniteScroll: true,
2020-07-06 16:08:30 +09:00
fixedWidgetsPosition: false,
roomGraphicsQuality: 'medium',
roomUseOrthographicCamera: true,
2020-02-20 02:40:53 +09:00
sfxVolume: 0.3,
2020-02-20 03:14:17 +09:00
sfxNote: 'syuilo/down',
sfxNoteMy: 'syuilo/up',
2020-02-20 02:40:53 +09:00
sfxNotification: 'syuilo/pope2',
2020-02-20 03:14:17 +09:00
sfxChat: 'syuilo/pope1',
sfxChatBg: 'syuilo/waon',
2020-02-20 06:08:49 +09:00
sfxAntenna: 'syuilo/triple',
userData: {},
};
function copy<T>(data: T): T {
return JSON.parse(JSON.stringify(data));
}
2020-03-31 09:11:43 +09:00
export default () => new Vuex.Store({
plugins: [createPersistedState({
paths: ['i', 'device', 'deviceUser', 'settings', 'instance']
})],
state: {
i: null,
2020-03-31 09:11:43 +09:00
pendingApiRequestsCount: 0,
spinner: null
},
getters: {
isSignedIn: state => state.i != null,
nav: (state, getters) => actions => ({
notifications: {
title: 'notifications',
icon: faBell,
get show() { return getters.isSignedIn; },
get indicated() { return getters.isSignedIn && state.i.hasUnreadNotification; },
to: '/my/notifications',
},
messaging: {
title: 'messaging',
icon: faComments,
get show() { return getters.isSignedIn; },
get indicated() { return getters.isSignedIn && state.i.hasUnreadMessagingMessage; },
to: '/my/messaging',
},
drive: {
title: 'drive',
icon: faCloud,
get show() { return getters.isSignedIn; },
to: '/my/drive',
},
followRequests: {
title: 'followRequests',
icon: faUserClock,
get show() { return getters.isSignedIn && state.i.isLocked; },
get indicated() { return getters.isSignedIn && state.i.hasPendingReceivedFollowRequest; },
to: '/my/follow-requests',
},
featured: {
title: 'featured',
icon: faFireAlt,
to: '/featured',
},
explore: {
title: 'explore',
icon: faHashtag,
to: '/explore',
},
announcements: {
title: 'announcements',
icon: faBroadcastTower,
get indicated() { return getters.isSignedIn && state.i.hasUnreadAnnouncement; },
to: '/announcements',
},
search: {
title: 'search',
icon: faSearch,
action: () => actions.search(),
},
lists: {
title: 'lists',
icon: faListUl,
get show() { return getters.isSignedIn; },
to: '/my/lists',
},
groups: {
title: 'groups',
icon: faUsers,
get show() { return getters.isSignedIn; },
to: '/my/groups',
},
antennas: {
title: 'antennas',
icon: faSatellite,
get show() { return getters.isSignedIn; },
to: '/my/antennas',
},
mentions: {
title: 'mentions',
icon: faAt,
get show() { return getters.isSignedIn; },
get indicated() { return getters.isSignedIn && state.i.hasUnreadMentions; },
to: '/my/mentions',
},
messages: {
title: 'directNotes',
icon: faEnvelope,
get show() { return getters.isSignedIn; },
get indicated() { return getters.isSignedIn && state.i.hasUnreadSpecifiedNotes; },
to: '/my/messages',
},
favorites: {
title: 'favorites',
icon: faStar,
get show() { return getters.isSignedIn; },
to: '/my/favorites',
},
pages: {
title: 'pages',
icon: faFileAlt,
get show() { return getters.isSignedIn; },
to: '/my/pages',
},
games: {
title: 'games',
icon: faGamepad,
to: '/games',
},
scratchpad: {
title: 'scratchpad',
icon: faTerminal,
to: '/scratchpad',
},
rooms: {
title: 'rooms',
icon: faDoorClosed,
get show() { return getters.isSignedIn; },
get to() { return `/@${state.i.username}/room`; },
},
}),
},
mutations: {
updateI(state, x) {
state.i = x;
},
updateIKeyValue(state, x) {
state.i[x.key] = x.value;
},
},
actions: {
async login(ctx, i) {
ctx.commit('updateI', i);
ctx.commit('settings/init', i.clientData);
ctx.commit('deviceUser/init', ctx.state.device.userData[i.id] || {});
await ctx.dispatch('addAcount', { id: i.id, i: localStorage.getItem('i') });
},
addAcount(ctx, info) {
if (!ctx.state.device.accounts.some(x => x.id === info.id)) {
ctx.commit('device/set', {
key: 'accounts',
value: ctx.state.device.accounts.concat([{ id: info.id, token: info.i }])
});
}
},
logout(ctx) {
ctx.commit('device/setUserData', { userId: ctx.state.i.id, data: ctx.state.deviceUser });
ctx.commit('updateI', null);
ctx.commit('settings/init', {});
ctx.commit('deviceUser/init', {});
localStorage.removeItem('i');
document.cookie = `igi=; path=/`;
},
async switchAccount(ctx, i) {
ctx.commit('device/setUserData', { userId: ctx.state.i.id, data: ctx.state.deviceUser });
localStorage.setItem('i', i.token);
await ctx.dispatch('login', i);
},
mergeMe(ctx, me) {
for (const [key, value] of Object.entries(me)) {
ctx.commit('updateIKeyValue', { key, value });
}
if (me.clientData) {
ctx.commit('settings/init', me.clientData);
}
},
2020-03-31 09:11:43 +09:00
api(ctx, { endpoint, data, token }) {
if (++ctx.state.pendingApiRequestsCount === 1) {
// TODO: spinnerの表示はstoreでやらない
ctx.state.spinner = document.createElement('div');
ctx.state.spinner.setAttribute('id', 'wait');
document.body.appendChild(ctx.state.spinner);
}
2020-04-02 05:41:03 +09:00
2020-03-31 09:11:43 +09:00
const onFinally = () => {
if (--ctx.state.pendingApiRequestsCount === 0) ctx.state.spinner.parentNode.removeChild(ctx.state.spinner);
};
2020-04-02 05:41:03 +09:00
2020-03-31 09:11:43 +09:00
const promise = new Promise((resolve, reject) => {
// Append a credential
if (ctx.getters.isSignedIn) (data as any).i = ctx.state.i.token;
2020-04-12 19:38:19 +09:00
if (token !== undefined) (data as any).i = token;
2020-04-02 05:41:03 +09:00
2020-03-31 09:11:43 +09:00
// Send request
fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
method: 'POST',
body: JSON.stringify(data),
credentials: 'omit',
cache: 'no-cache'
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
2020-04-02 05:41:03 +09:00
2020-03-31 09:11:43 +09:00
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve();
} else {
reject(body.error);
}
}).catch(reject);
});
2020-04-02 05:41:03 +09:00
2020-03-31 09:11:43 +09:00
promise.then(onFinally, onFinally);
2020-04-02 05:41:03 +09:00
2020-03-31 09:11:43 +09:00
return promise;
}
},
modules: {
instance: {
namespaced: true,
state: {
meta: null
},
mutations: {
set(state, meta) {
state.meta = meta;
},
},
actions: {
async fetch(ctx) {
2020-03-31 09:11:43 +09:00
const meta = await ctx.dispatch('api', {
endpoint: 'meta',
data: {
detail: false
}
}, { root: true });
ctx.commit('set', meta);
}
}
},
device: {
namespaced: true,
state: defaultDeviceSettings,
mutations: {
set(state, x: { key: string; value: any }) {
state[x.key] = x.value;
},
setUserData(state, x: { userId: string; data: any }) {
state.userData[x.userId] = copy(x.data);
},
setInfiniteScrollEnabling(state, x: boolean) {
state.enableInfiniteScroll = x;
},
}
},
deviceUser: {
namespaced: true,
state: defaultDeviceUserSettings,
mutations: {
init(state, x) {
for (const [key, value] of Object.entries(defaultDeviceUserSettings)) {
if (x[key]) {
state[key] = x[key];
} else {
state[key] = value;
}
}
},
set(state, x: { key: string; value: any }) {
state[x.key] = x.value;
},
setTl(state, x) {
state.tl = {
src: x.src,
arg: x.arg
};
},
setMenu(state, menu) {
state.menu = menu;
},
setVisibility(state, visibility) {
state.visibility = visibility;
},
2020-02-05 09:42:58 +09:00
setLocalOnly(state, localOnly) {
state.localOnly = localOnly;
},
setWidgets(state, widgets) {
state.widgets = widgets;
},
addWidget(state, widget) {
state.widgets.unshift(widget);
},
removeWidget(state, widget) {
state.widgets = state.widgets.filter(w => w.id != widget.id);
},
updateWidget(state, x) {
2020-04-04 08:46:54 +09:00
const w = state.widgets.find(w => w.id === x.id);
if (w) {
w.data = x.data;
}
},
}
},
settings: {
namespaced: true,
state: defaultSettings,
mutations: {
set(state, x: { key: string; value: any }) {
nestedProperty.set(state, x.key, x.value);
},
init(state, x) {
for (const [key, value] of Object.entries(defaultSettings)) {
if (x[key]) {
state[key] = x[key];
} else {
state[key] = value;
}
}
},
},
actions: {
set(ctx, x) {
ctx.commit('set', x);
if (ctx.rootGetters.isSignedIn) {
2020-03-31 09:11:43 +09:00
ctx.dispatch('api', {
endpoint: 'i/update-client-setting',
data: {
name: x.key,
value: x.value
}
}, { root: true });
}
},
}
}
}
});