マルチアカウント対応

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",
"version": "1.5.1",
"name": "misskey-tools",
"version": "2.0.0",
"description": "",
"main": "built/app.js",
"author": "Xeltica",

View File

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

View File

@ -31,7 +31,14 @@ html
if token
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, '/');
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>
<strong>{t('instanceUrl')}</strong>
</div>
<div className="hgroup">
<div className="hgroup login-form">
<input
className="input-field"
type="text"

View File

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

View File

@ -59,6 +59,7 @@
},
"_nav": {
"misshai": "ミス廃",
"accounts": "アカウント",
"settings": "設定"
},
"_missHai": {
@ -73,6 +74,11 @@
"order": "順位",
"showRanking": "ランキングを見る"
},
"_accounts": {
"currentAccount": "現在ログインしているアカウント",
"switchAccount": "アカウント切り替え",
"useAnother": "他のアカウントで登録する"
},
"_developerInfo": {
"title": "開発者",
"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 { MisshaiPage } from '../components/MisshaiPage';
import { Tab, TabItem } from '../components/Tab';
import { SettingPage } from '../components/SettingPage';
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 = () => {
const [selectedTab, setSelectedTab] = useState<number>(0);
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[]>(() => ([
{ label: t('_nav.misshai') },
{ label: t('_nav.accounts') },
{ label: t('_nav.settings') },
]), [i18n.language]);
const component = useMemo(() => {
switch (selectedTab) {
case 0: return <MisshaiPage />;
case 1: return <SettingPage/>;
case 1: return <AccountsPage />;
case 2: return <SettingPage/>;
default: return null;
}
}, [selectedTab]);

View File

@ -35,7 +35,7 @@ export const IndexWelcomePage: React.VFC = () => {
<article>
<h3>{t('_welcome.misshaiAlertTitle')}</h3>
<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">
<pre>{example}</pre>
</div>

View File

@ -1,7 +1,8 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
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 { Modal } from '../../modal/modal';
@ -10,6 +11,8 @@ interface ScreenState {
modalShown: boolean;
theme: Theme;
language: string;
accounts: IUser[];
accountTokens: string[];
}
const initialState: ScreenState = {
@ -17,6 +20,8 @@ const initialState: ScreenState = {
modalShown: false,
theme: localStorage[LOCALSTORAGE_KEY_THEME] ?? 'system',
language: localStorage[LOCALSTORAGE_KEY_LANG] ?? i18n.language ?? 'ja_JP',
accounts: [],
accountTokens: JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[],
};
export const screenSlice = createSlice({
@ -40,9 +45,14 @@ export const screenSlice = createSlice({
localStorage[LOCALSTORAGE_KEY_LANG] = 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;

View File

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