マルチアカウント対応

This commit is contained in:
xeltica 2021-10-09 02:11:00 +09:00
parent 24b419fe60
commit 8a5daa866c
11 changed files with 128 additions and 10 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "misshaialert", "name": "misskey-tools",
"version": "1.5.1", "version": "2.0.0",
"description": "", "description": "",
"main": "built/app.js", "main": "built/app.js",
"author": "Xeltica", "author": "Xeltica",

View File

@ -1,5 +1,5 @@
export default { export default {
version: '1.5.1', version: '2.0.0',
changelog: [ changelog: [
'インスタンスの接続エラーにより後続処理が行えなくなる重大な不具合を修正', 'インスタンスの接続エラーにより後続処理が行えなくなる重大な不具合を修正',
'全員分の算出が終わるまで、ランキングを非表示に', '全員分の算出が終わるまで、ランキングを非表示に',

View File

@ -31,7 +31,14 @@ html
if token if token
script. script.
localStorage.setItem('token', '#{token}'); const token = '#{token}';
const previousToken = localStorage.getItem('token');
const accounts = JSON.parse(localStorage.getItem('accounts') && '[]');
if (previousToken && !accounts.includes(previousToken)) {
accounts.push(previousToken);
}
localStorage.setItem('accounts', JSON.stringify(accounts));
localStorage.setItem('token', token);
history.replaceState(null, null, '/'); history.replaceState(null, null, '/');
if error if error

View File

@ -0,0 +1,67 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_TOKEN } from '../const';
import { useGetSessionQuery } from '../services/session';
import { useSelector } from '../store';
import { setAccounts } from '../store/slices/screen';
import { LoginForm } from './LoginForm';
import { Skeleton } from './Skeleton';
export const AccountsPage: React.VFC = () => {
const {data} = useGetSessionQuery(undefined);
const {t} = useTranslation();
const dispatch = useDispatch();
const {accounts, accountTokens} = useSelector(state => state.screen);
const switchAccount = (token: string) => {
const newAccounts = accountTokens.filter(a => a !== token);
newAccounts.push(localStorage.getItem(LOCALSTORAGE_KEY_TOKEN) ?? '');
localStorage.setItem(LOCALSTORAGE_KEY_ACCOUNTS, JSON.stringify(newAccounts));
localStorage.setItem(LOCALSTORAGE_KEY_TOKEN, token);
location.reload();
};
return !data ? (
<div className="vstack">
<Skeleton />
<Skeleton />
<Skeleton />
</div>
) : (
<div className="fade vstack">
<div className="card">
<div className="body">
<h1>{t('_accounts.currentAccount')}</h1>
<p>@{data.username}@{data.host}</p>
</div>
</div>
<article>
<h2>{t('_accounts.switchAccount')}</h2>
<div className="menu large fluid mb-2">
{
accounts.length === accountTokens.length ? (
accounts.map(account => (
<button className="item fluid" style={{display: 'flex', flexDirection: 'row', alignItems: 'center'}} onClick={() => switchAccount(account.misshaiToken)}>
<i className="icon bi bi-chevron-right" />
@{account.username}@{account.host}
<button className="btn flat text-danger" style={{marginLeft: 'auto'}} onClick={e => {
const filteredAccounts = accounts.filter(ac => ac.id !== account.id);
dispatch(setAccounts(filteredAccounts));
e.stopPropagation();
}}>
<i className="bi bi-trash"/>
</button>
</button>
))
) : (
<div className="item">...</div>
)
}
</div>
<LoginForm />
</article>
</div>
);
};

View File

@ -10,7 +10,7 @@ export const LoginForm: React.VFC = () => {
<div> <div>
<strong>{t('instanceUrl')}</strong> <strong>{t('instanceUrl')}</strong>
</div> </div>
<div className="hgroup"> <div className="hgroup login-form">
<input <input
className="input-field" className="input-field"
type="text" type="text"

View File

@ -1,5 +1,6 @@
export const LOCALSTORAGE_KEY_TOKEN = 'token'; export const LOCALSTORAGE_KEY_TOKEN = 'token';
export const LOCALSTORAGE_KEY_THEME = 'theme'; export const LOCALSTORAGE_KEY_THEME = 'theme';
export const LOCALSTORAGE_KEY_LANG = 'lang'; export const LOCALSTORAGE_KEY_LANG = 'lang';
export const LOCALSTORAGE_KEY_ACCOUNTS = 'accounts';
export const API_ENDPOINT = `//${location.host}/api/v1/`; export const API_ENDPOINT = `//${location.host}/api/v1/`;

View File

@ -59,6 +59,7 @@
}, },
"_nav": { "_nav": {
"misshai": "ミス廃", "misshai": "ミス廃",
"accounts": "アカウント",
"settings": "設定" "settings": "設定"
}, },
"_missHai": { "_missHai": {
@ -73,6 +74,11 @@
"order": "順位", "order": "順位",
"showRanking": "ランキングを見る" "showRanking": "ランキングを見る"
}, },
"_accounts": {
"currentAccount": "現在ログインしているアカウント",
"switchAccount": "アカウント切り替え",
"useAnother": "他のアカウントで登録する"
},
"_developerInfo": { "_developerInfo": {
"title": "開発者", "title": "開発者",
"description": "何か困ったことがあったら、以下のアカウントにメッセージを送ってください。" "description": "何か困ったことがあったら、以下のアカウントにメッセージを送ってください。"

View File

@ -1,25 +1,47 @@
import React, { useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { Header } from '../components/Header'; import { Header } from '../components/Header';
import { MisshaiPage } from '../components/MisshaiPage'; import { MisshaiPage } from '../components/MisshaiPage';
import { Tab, TabItem } from '../components/Tab'; import { Tab, TabItem } from '../components/Tab';
import { SettingPage } from '../components/SettingPage'; import { SettingPage } from '../components/SettingPage';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AccountsPage } from '../components/AccountsPage';
import { useDispatch } from 'react-redux';
import { API_ENDPOINT, LOCALSTORAGE_KEY_ACCOUNTS } from '../const';
import { IUser } from '../../common/types/user';
import { setAccounts } from '../store/slices/screen';
const getSession = (token: string) => {
return fetch(`${API_ENDPOINT}session`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
}).then(r => r.json()).then(r => r as IUser);
};
export const IndexSessionPage: React.VFC = () => { export const IndexSessionPage: React.VFC = () => {
const [selectedTab, setSelectedTab] = useState<number>(0); const [selectedTab, setSelectedTab] = useState<number>(0);
const {t, i18n} = useTranslation(); const {t, i18n} = useTranslation();
const dispatch = useDispatch();
useEffect(() => {
const accounts = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[];
Promise.all(accounts.map(getSession)).then(a => dispatch(setAccounts(a)));
}, [dispatch]);
const items = useMemo<TabItem[]>(() => ([ const items = useMemo<TabItem[]>(() => ([
{ label: t('_nav.misshai') }, { label: t('_nav.misshai') },
{ label: t('_nav.accounts') },
{ label: t('_nav.settings') }, { label: t('_nav.settings') },
]), [i18n.language]); ]), [i18n.language]);
const component = useMemo(() => { const component = useMemo(() => {
switch (selectedTab) { switch (selectedTab) {
case 0: return <MisshaiPage />; case 0: return <MisshaiPage />;
case 1: return <SettingPage/>; case 1: return <AccountsPage />;
case 2: return <SettingPage/>;
default: return null; default: return null;
} }
}, [selectedTab]); }, [selectedTab]);

View File

@ -35,7 +35,7 @@ export const IndexWelcomePage: React.VFC = () => {
<article> <article>
<h3>{t('_welcome.misshaiAlertTitle')}</h3> <h3>{t('_welcome.misshaiAlertTitle')}</h3>
<p>{t('_welcome.misshaiAlertDescription')}</p> <p>{t('_welcome.misshaiAlertDescription')}</p>
<div className="card ma-2 shadow-2" style={{maxWidth: 320}}> <div className="card ma-2 shadow-2" style={{maxWidth: 360}}>
<div className="body"> <div className="body">
<pre>{example}</pre> <pre>{example}</pre>
</div> </div>

View File

@ -1,7 +1,8 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import i18n from 'i18next'; import i18n from 'i18next';
import { IUser } from '../../../common/types/user';
import { LOCALSTORAGE_KEY_LANG, LOCALSTORAGE_KEY_THEME } from '../../const'; import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_LANG, LOCALSTORAGE_KEY_THEME } from '../../const';
import { Theme } from '../../misc/theme'; import { Theme } from '../../misc/theme';
import { Modal } from '../../modal/modal'; import { Modal } from '../../modal/modal';
@ -10,6 +11,8 @@ interface ScreenState {
modalShown: boolean; modalShown: boolean;
theme: Theme; theme: Theme;
language: string; language: string;
accounts: IUser[];
accountTokens: string[];
} }
const initialState: ScreenState = { const initialState: ScreenState = {
@ -17,6 +20,8 @@ const initialState: ScreenState = {
modalShown: false, modalShown: false,
theme: localStorage[LOCALSTORAGE_KEY_THEME] ?? 'system', theme: localStorage[LOCALSTORAGE_KEY_THEME] ?? 'system',
language: localStorage[LOCALSTORAGE_KEY_LANG] ?? i18n.language ?? 'ja_JP', language: localStorage[LOCALSTORAGE_KEY_LANG] ?? i18n.language ?? 'ja_JP',
accounts: [],
accountTokens: JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[],
}; };
export const screenSlice = createSlice({ export const screenSlice = createSlice({
@ -40,9 +45,14 @@ export const screenSlice = createSlice({
localStorage[LOCALSTORAGE_KEY_LANG] = action.payload; localStorage[LOCALSTORAGE_KEY_LANG] = action.payload;
i18n.changeLanguage(action.payload); i18n.changeLanguage(action.payload);
}, },
setAccounts: (state, action: PayloadAction<ScreenState['accounts']>) => {
state.accounts = action.payload;
state.accountTokens = action.payload.map(a => a.misshaiToken);
localStorage[LOCALSTORAGE_KEY_ACCOUNTS] = JSON.stringify(state.accountTokens);
},
}, },
}); });
export const { showModal, hideModal, changeTheme, changeLang } = screenSlice.actions; export const { showModal, hideModal, changeTheme, changeLang, setAccounts } = screenSlice.actions;
export default screenSlice.reducer; export default screenSlice.reducer;

View File

@ -92,3 +92,8 @@ small {
padding: calc(var(--margin) / 2); padding: calc(var(--margin) / 2);
background: var(--panel); background: var(--panel);
} }
.login-form {
background: var(--panel);
border-radius: var(--radius);
}