Compare commits

..

16 Commits

Author SHA1 Message Date
syuilo
89d5df20a5 9.1.0 2018-10-02 16:29:05 +09:00
MeiMei
c09a2a37fe リモートのピン留め投稿取得対応 (#2798)
* Fetch featured

* Handle featured change

* Fix typo
2018-10-02 16:27:36 +09:00
syuilo
b5745877ca 🎨 2018-10-02 16:23:55 +09:00
syuilo
6e04549a9b Enable JSON5 syntax 2018-10-02 16:13:14 +09:00
syuilo
38139ee6c9 テーマに関して強化 2018-10-02 16:10:45 +09:00
syuilo
6b96bd0185 テーマに関して強化 2018-10-02 16:04:31 +09:00
MeiMei
f2b9863eea Better deployment option descriptions. (#2800) 2018-10-02 11:59:12 +09:00
syuilo
f56adce51f 9.0.0 2018-10-02 00:34:37 +09:00
Hakaba Hitoyo
35362ed3c7 Better example settings for TLS certification (#2793) 2018-10-01 19:29:02 +09:00
MeiMei
cd0b9a8e3f 非承認制アカウントのリモートフォローではフォロー許可待ちと表示しない (#2796) 2018-10-01 19:28:24 +09:00
syuilo
110aadd65c 8.64.0 2018-10-01 00:15:17 +09:00
syuilo
ff76c815b1 テーマインストール時の動作をわかりやすくしたりテーマをアンインストールできるようにしたり 2018-09-30 23:03:21 +09:00
Acid Chicken (硫酸鶏)
1b9b8912ae Update post-form.vue (#2790) 2018-09-30 22:45:24 +09:00
syuilo
579b61a806 Update tests 2018-09-30 14:46:18 +09:00
MeiMei
1e2b484929 profile banner が濃すぎるのを修正 (#2783) 2018-09-29 16:52:58 +09:00
syuilo
25dd19dd8c 🎨 2018-09-29 09:11:06 +09:00
45 changed files with 932 additions and 699 deletions

View File

@@ -7,27 +7,51 @@ maintainer:
repository_url: https://github.com/syuilo/misskey # Repository URL
feedback_url: https://github.com/syuilo/misskey/issues # Feedback URL (e.g. github issue)
# URL and Port settings overview
# e.g., If you want to realize following structure:
#
# +--- https://example.com:123 ----------+
# +------+ |+-------------+ +---------------+|
# | User | ---> || Proxy (123) | ---> | Misskey (456) ||
# +------+ |+-------------+ +---------------+|
# +--------------------------------------+
#
# You need to set 'https://example.com:123' to 'url' prop and
# You need to set 456 to 'port' prop.
#
# In other words, the 'url' prop should be the final accessible URL seen by a user.
# 'port' prop is a port that the Misskey server should actually listen
# on and it is not necessarily the port that a user accesses.
url: http://localhost/
# Final accessible URL seen by a user.
url: https://example.tld/
### Port and TLS settings ######################################
#
# Misskey supports two deployment options for public.
#
# Option 1: With Reverse Proxy
#
# +----- https://example.tld/ ------------+
# +------+ |+-------------+ +----------------+|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
# +------+ |+-------------+ +----------------+|
# +---------------------------------------+
#
# You need to setup reverse proxy. (eg. Nginx)
# You do not define 'https' section.
# Option 2: Standalone
#
# +- https://example.tld/ -+
# +------+ | +---------------+ |
# | User | ---> | | Misskey (443) | |
# +------+ | +---------------+ |
# +------------------------+
#
# You need to run Misskey as root.
# You need to set Certificate in 'https' section.
# To use option 1, uncomment below line.
# port: 3000 # A port that your Misskey server should listen.
# To use option 2, uncomment below lines.
# port: 443
#
# https:
# # path for certification
# key: /etc/letsencrypt/live/example.tld/privkey.pem
# cert: /etc/letsencrypt/live/example.tld/fullchain.pem
################################################################
# A port that your Misskey server should listen.
# This value is not a port to use when accessing with a browser.
port: 80
mongodb:
host: localhost
@@ -98,12 +122,6 @@ drive:
# Below settings are optional
#
# TLS
# https:
# # path for certification
# key: example-tls-key
# cert: example-tls-cert
# Elasticsearch
# elasticsearch:
# host: localhost

1
.gitattributes vendored
View File

@@ -3,3 +3,4 @@
*.ai -diff -text
yarn.lock -diff -text
package-lock.json -diff -text
*.json5 linguist-language=JavaScript

View File

@@ -5,6 +5,12 @@ ChangeLog
This document describes breaking changes only.
9.0.0
-----
Misskey v8.64.0 を使っている方は、9.0.0に際しては特にすべきことはありません。
Misskey v8.64.0 に満たないバージョンをお使いの方は、一旦8.64.0にアップデートして(そして起動して)から9.0.0に再度アップデートしてください。
8.0.0
-----

View File

@@ -439,6 +439,7 @@ common/views/pages/follow.vue:
following: "Following"
follow: "Follow"
request-pending: "Pending follow request"
follow-processing: "Processing follow"
follow-request: "Follow request"
desktop:
banner-crop-title: "Crop the part that appears as a banner"
@@ -565,6 +566,7 @@ desktop/views/components/follow-button.vue:
following: "Following"
follow: "Follow"
request-pending: "Pending follow request"
follow-processing: "Processing follow"
follow-request: "Follow request"
desktop/views/components/followers-window.vue:
followers: "{}'s followers"
@@ -1044,6 +1046,7 @@ mobile/views/components/follow-button.vue:
following: "Following"
follow: "Follow"
request-pending: "Pending follow request"
follow-processing: "Processing follow"
follow-request: "Follow request"
mobile/views/components/friends-maker.vue:
title: "Let's follow them"

View File

@@ -291,6 +291,7 @@ common/views/components/theme.vue:
install-a-theme: "テーマのインストール"
theme-code: "テーマコード"
install: "インストール"
installed: "「{}」をインストールしました"
create-a-theme: "テーマの作成"
save-created-theme: "テーマを保存"
primary-color: "プライマリ カラー"
@@ -306,6 +307,10 @@ common/views/components/theme.vue:
saved: "保存しました"
installed-themes: "インストールされたテーマ"
select-theme: "テーマを選択してください"
uninstall: "アンインストール"
uninstalled: "「{}」をアンインストールしました"
author: "作者"
desc: "説明"
common/views/components/cw-button.vue:
hide: "隠す"
@@ -511,6 +516,7 @@ common/views/pages/follow.vue:
following: "フォロー中"
follow: "フォロー"
request-pending: "フォロー許可待ち"
follow-processing: "フォロー処理中"
follow-request: "フォロー申請"
desktop:
@@ -653,6 +659,7 @@ desktop/views/components/follow-button.vue:
following: "フォロー中"
follow: "フォロー"
request-pending: "フォロー許可待ち"
follow-processing: "フォロー処理中"
follow-request: "フォロー申請"
desktop/views/components/followers-window.vue:
@@ -1229,6 +1236,7 @@ mobile/views/components/follow-button.vue:
following: "フォロー中"
follow: "フォロー"
request-pending: "フォロー許可待ち"
follow-processing: "フォロー処理中"
follow-request: "フォロー申請"
mobile/views/components/friends-maker.vue:

View File

@@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "8.63.0",
"clientVersion": "1.0.10040",
"version": "9.1.0",
"clientVersion": "1.0.10056",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,
@@ -134,6 +134,8 @@
"is-url": "1.2.4",
"js-yaml": "3.12.0",
"jsdom": "11.12.0",
"json5": "2.1.0",
"json5-loader": "1.0.1",
"koa": "2.5.1",
"koa-bodyparser": "4.2.1",
"koa-compress": "3.0.0",

View File

@@ -5,9 +5,6 @@
<script lang="ts">
import Vue from 'vue';
import { url, lang } from './config';
import applyTheme from './common/scripts/theme';
const darkTheme = require('../theme/dark');
const halloweenTheme = require('../theme/halloween');
export default Vue.extend({
computed: {

View File

@@ -24,7 +24,6 @@
const theme = localStorage.getItem('theme');
if (theme) {
Object.entries(JSON.parse(theme)).forEach(([k, v]) => {
if (k == 'meta') return;
document.documentElement.style.setProperty(`--${k}`, v.toString());
});
}

View File

@@ -1,16 +1,16 @@
<template>
<form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit">
<div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div>
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange">
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange" styl="fill">
<span>%i18n:@username%</span>
<span slot="prefix">@</span>
<span slot="suffix">@{{ host }}</span>
</ui-input>
<ui-input v-model="password" type="password" required>
<ui-input v-model="password" type="password" required styl="fill">
<span>%i18n:@password%</span>
<span slot="prefix">%fa:lock%</span>
</ui-input>
<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required/>
<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required styl="fill"/>
<ui-button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</ui-button>
<p style="margin: 8px 0;">%i18n:@or% <a :href="`${apiUrl}/signin/twitter`">%i18n:@signin-with-twitter%</a></p>
</form>

View File

@@ -1,12 +1,12 @@
<template>
<form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()">
<template v-if="meta">
<ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
<ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required styl="fill">
<span>%i18n:@invitation-code%</span>
<span slot="prefix">%fa:id-card-alt%</span>
<p slot="text" v-html="'%i18n:@invitation-info%'.replace('{}', meta.maintainer.url)"></p>
</ui-input>
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername">
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername" styl="fill">
<span>%i18n:@username%</span>
<span slot="prefix">@</span>
<span slot="suffix">@{{ host }}</span>
@@ -18,7 +18,7 @@
<p slot="text" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-short%</p>
<p slot="text" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw% %i18n:@too-long%</p>
</ui-input>
<ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true">
<ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true" styl="fill">
<span>%i18n:@password%</span>
<span slot="prefix">%fa:lock%</span>
<div slot="text">
@@ -27,7 +27,7 @@
<p slot="text" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw% %i18n:@strong-password%</p>
</div>
</ui-input>
<ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype">
<ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype" styl="fill">
<span>%i18n:@password% (%i18n:@retype%)</span>
<span slot="prefix">%fa:lock%</span>
<div slot="text">

View File

@@ -3,19 +3,19 @@
<label>
<span>%i18n:@light-theme%</span>
<ui-select v-model="light" placeholder="%i18n:@light-theme%">
<option v-for="x in themes" :value="x.meta.id" :key="x.meta.id">{{ x.meta.name }}</option>
<option v-for="x in themes" :value="x.id" :key="x.id">{{ x.name }}</option>
</ui-select>
</label>
<label>
<span>%i18n:@dark-theme%</span>
<ui-select v-model="dark" placeholder="%i18n:@dark-theme%">
<option v-for="x in themes" :value="x.meta.id" :key="x.meta.id">{{ x.meta.name }}</option>
<option v-for="x in themes" :value="x.id" :key="x.id">{{ x.name }}</option>
</ui-select>
</label>
<details class="creator">
<summary>%i18n:@create-a-theme%</summary>
<summary>%fa:palette% %i18n:@create-a-theme%</summary>
<div>
<span>%i18n:@base-theme%:</span>
<ui-radio v-model="myThemeBase" value="light">%i18n:@base-theme-light%</ui-radio>
@@ -38,37 +38,60 @@
<div style="padding-bottom:8px;">%i18n:@text-color%:</div>
<color-picker v-model="myThemeText"/>
</div>
<ui-button @click="preview()">%i18n:@preview-created-theme%</ui-button>
<ui-button primary @click="gen()">%i18n:@save-created-theme%</ui-button>
<ui-button @click="preview()">%fa:eye% %i18n:@preview-created-theme%</ui-button>
<ui-button primary @click="gen()">%fa:save R% %i18n:@save-created-theme%</ui-button>
</details>
<details>
<summary>%i18n:@install-a-theme%</summary>
<summary>%fa:download% %i18n:@install-a-theme%</summary>
<ui-textarea v-model="installThemeCode">
<span>%i18n:@theme-code%</span>
</ui-textarea>
<ui-button @click="install()">%i18n:@install%</ui-button>
<ui-button @click="install()">%fa:check% %i18n:@install%</ui-button>
</details>
<details>
<summary>%i18n:@installed-themes%</summary>
<ui-select v-model="selectedInstalledTheme" placeholder="%i18n:@select-theme%">
<option v-for="x in installedThemes" :value="x.meta.id" :key="x.meta.id">{{ x.meta.name }}</option>
<summary>%fa:folder-open% %i18n:@installed-themes%</summary>
<ui-select v-model="selectedInstalledThemeId" placeholder="%i18n:@select-theme%">
<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</ui-select>
<ui-textarea readonly :value="selectedInstalledThemeCode">
<span>%i18n:@theme-code%</span>
</ui-textarea>
<template v-if="selectedInstalledTheme">
<ui-input readonly :value="selectedInstalledTheme.author">
<span>%i18n:@author%</span>
</ui-input>
<ui-textarea v-if="selectedInstalledTheme.desc" readonly :value="selectedInstalledTheme.desc">
<span>%i18n:@desc%</span>
</ui-textarea>
<ui-textarea readonly :value="selectedInstalledThemeCode">
<span>%i18n:@theme-code%</span>
</ui-textarea>
<ui-button @click="uninstall()">%fa:trash-alt R% %i18n:@uninstall%</ui-button>
</template>
</details>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { apiUrl, docsUrl } from '../../../config';
import { lightTheme, darkTheme, builtinThemes, applyTheme } from '../../../theme';
import { lightTheme, darkTheme, builtinThemes, applyTheme, Theme } from '../../../theme';
import { Chrome } from 'vue-color';
import * as uuid from 'uuid';
import * as tinycolor from 'tinycolor2';
import * as JSON5 from 'json5';
// 後方互換性のため
function convertOldThemedefinition(t) {
const t2 = {
id: t.meta.id,
name: t.meta.name,
author: t.meta.author,
base: t.meta.base,
vars: t.meta.vars,
props: t
};
delete t2.props.meta;
return t2;
}
export default Vue.extend({
components: {
@@ -78,21 +101,21 @@ export default Vue.extend({
data() {
return {
installThemeCode: null,
selectedInstalledTheme: null,
selectedInstalledThemeId: null,
myThemeBase: 'light',
myThemeName: '',
myThemePrimary: lightTheme.meta.vars.primary,
myThemeSecondary: lightTheme.meta.vars.secondary,
myThemeText: lightTheme.meta.vars.text
myThemePrimary: lightTheme.vars.primary,
myThemeSecondary: lightTheme.vars.secondary,
myThemeText: lightTheme.vars.text
};
},
computed: {
themes(): any {
themes(): Theme[] {
return this.$store.state.device.themes.concat(builtinThemes);
},
installedThemes(): any {
installedThemes(): Theme[] {
return this.$store.state.device.themes;
},
@@ -106,22 +129,25 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'darkTheme', value }); }
},
selectedInstalledTheme() {
if (this.selectedInstalledThemeId == null) return null;
return this.installedThemes.find(x => x.id == this.selectedInstalledThemeId);
},
selectedInstalledThemeCode() {
if (this.selectedInstalledTheme == null) return null;
return JSON.stringify(this.installedThemes.find(x => x.meta.id == this.selectedInstalledTheme));
return JSON5.stringify(this.selectedInstalledTheme, null, '\t');
},
myTheme(): any {
return {
meta: {
name: this.myThemeName,
author: this.$store.state.i.name,
base: this.myThemeBase,
vars: {
primary: tinycolor(typeof this.myThemePrimary == 'string' ? this.myThemePrimary : this.myThemePrimary.rgba).toRgbString(),
secondary: tinycolor(typeof this.myThemeSecondary == 'string' ? this.myThemeSecondary : this.myThemeSecondary.rgba).toRgbString(),
text: tinycolor(typeof this.myThemeText == 'string' ? this.myThemeText : this.myThemeText.rgba).toRgbString()
}
name: this.myThemeName,
author: this.$store.state.i.username,
base: this.myThemeBase,
vars: {
primary: tinycolor(typeof this.myThemePrimary == 'string' ? this.myThemePrimary : this.myThemePrimary.rgba).toRgbString(),
secondary: tinycolor(typeof this.myThemeSecondary == 'string' ? this.myThemeSecondary : this.myThemeSecondary.rgba).toRgbString(),
text: tinycolor(typeof this.myThemeText == 'string' ? this.myThemeText : this.myThemeText.rgba).toRgbString()
}
};
}
@@ -130,27 +156,67 @@ export default Vue.extend({
watch: {
myThemeBase(v) {
const theme = v == 'light' ? lightTheme : darkTheme;
this.myThemePrimary = theme.meta.vars.primary;
this.myThemeSecondary = theme.meta.vars.secondary;
this.myThemeText = theme.meta.vars.text;
this.myThemePrimary = theme.vars.primary;
this.myThemeSecondary = theme.vars.secondary;
this.myThemeText = theme.vars.text;
}
},
beforeCreate() {
// migrate old theme definitions
// 後方互換性のため
this.$store.commit('device/set', {
key: 'themes', value: this.$store.state.device.themes.map(t => {
if (t.id == null) {
return convertOldThemedefinition(t);
} else {
return t;
}
})
});
},
methods: {
install() {
const theme = JSON.parse(this.installThemeCode);
if (theme.meta == null || theme.meta.id == null) {
let theme;
try {
theme = JSON5.parse(this.installThemeCode);
} catch (e) {
alert('%i18n:@invalid-theme%');
return;
}
if (this.$store.state.device.themes.some(t => t.meta.id == theme.meta.id)) {
// 後方互換性のため
if (theme.id == null && theme.meta != null) {
theme = convertOldThemedefinition(theme);
}
if (theme.id == null) {
alert('%i18n:@invalid-theme%');
return;
}
if (this.$store.state.device.themes.some(t => t.id == theme.id)) {
alert('%i18n:@already-installed%');
return;
}
const themes = this.$store.state.device.themes.concat(theme);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
alert('%i18n:@installed%'.replace('{}', theme.name));
},
uninstall() {
const theme = this.selectedInstalledTheme;
const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
alert('%i18n:@uninstalled%'.replace('{}', theme.name));
},
preview() {
@@ -159,7 +225,7 @@ export default Vue.extend({
gen() {
const theme = this.myTheme;
theme.meta.id = uuid();
theme.id = uuid();
const themes = this.$store.state.device.themes.concat(theme);
this.$store.commit('device/set', {
key: 'themes', value: themes
@@ -172,6 +238,11 @@ export default Vue.extend({
<style lang="stylus" scoped>
.nicnklzforebnpfgasiypmpdaaglujqm
> details
margin-top 16px
padding-top 16px
border-top solid 1px var(--faceDivider)
> .creator
> div
padding 16px 0

View File

@@ -71,14 +71,18 @@ export default Vue.extend({
type: Boolean,
required: false,
default: false
},
styl: {
type: String,
required: false,
default: 'line'
}
},
data() {
return {
v: this.value,
focused: false,
passwordStrength: '',
styl: 'fill'
passwordStrength: ''
};
},
computed: {
@@ -117,14 +121,6 @@ export default Vue.extend({
}
}
},
inject: {
isCardChild: { default: false }
},
created() {
if (this.isCardChild) {
this.styl = 'line';
}
},
mounted() {
if (this.$refs.prefix) {
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';

View File

@@ -29,13 +29,17 @@ export default Vue.extend({
required: {
type: Boolean,
required: false
},
styl: {
type: String,
required: false,
default: 'line'
}
},
data() {
return {
v: this.value,
focused: false,
styl: 'fill'
focused: false
};
},
computed: {
@@ -48,14 +52,6 @@ export default Vue.extend({
this.v = v;
}
},
inject: {
isCardChild: { default: false }
},
created() {
if (this.isCardChild) {
this.styl = 'line';
}
},
mounted() {
if (this.$refs.prefix) {
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';

View File

@@ -19,7 +19,8 @@
@click="onClick"
:disabled="followWait">
<template v-if="!followWait">
<template v-if="user.hasPendingFollowRequestFromYou">%fa:hourglass-half% %i18n:@request-pending%</template>
<template v-if="user.hasPendingFollowRequestFromYou && user.isLocked">%fa:hourglass-half% %i18n:@request-pending%</template>
<template v-else-if="user.hasPendingFollowRequestFromYou && !user.isLocked">%fa:hourglass-start% %i18n:@follow-processing%</template>
<template v-else-if="user.isFollowing">%fa:minus% %i18n:@following%</template>
<template v-else-if="!user.isFollowing && user.isLocked">%fa:plus% %i18n:@follow-request%</template>
<template v-else-if="!user.isFollowing && !user.isLocked">%fa:plus% %i18n:@follow%</template>

View File

@@ -5,7 +5,8 @@
:disabled="wait"
>
<template v-if="!wait">
<template v-if="u.hasPendingFollowRequestFromYou">%fa:hourglass-half%<template v-if="size == 'big'"> %i18n:@request-pending%</template></template>
<template v-if="u.hasPendingFollowRequestFromYou && u.isLocked">%fa:hourglass-half%<template v-if="size == 'big'"> %i18n:@request-pending%</template></template>
<template v-else-if="u.hasPendingFollowRequestFromYou && !u.isLocked">%fa:hourglass-start%<template v-if="size == 'big'"> %i18n:@follow-processing%</template></template>
<template v-else-if="u.isFollowing">%fa:minus%<template v-if="size == 'big'"> %i18n:@following%</template></template>
<template v-else-if="!u.isFollowing && u.isLocked">%fa:plus%<template v-if="size == 'big'"> %i18n:@follow-request%</template></template>
<template v-else-if="!u.isFollowing && !u.isLocked">%fa:plus%<template v-if="size == 'big'"> %i18n:@follow%</template></template>

View File

@@ -450,6 +450,7 @@ export default Vue.extend({
display block
padding 16px
background var(--desktopPostFormBg)
overflow hidden
&:after
content ""

View File

@@ -86,7 +86,7 @@ export default define({
> .banner
height 100px
background-color var(--primaryDarken10)
background-color var(--primaryAlpha01)
background-size cover
background-position center
cursor pointer

View File

@@ -14,8 +14,7 @@ import App from './app.vue';
import checkForUpdate from './common/scripts/check-for-update';
import MiOS, { API } from './mios';
import { version, codename, lang } from './config';
import { builtinThemes, applyTheme } from './theme';
const lightTheme = require('../theme/light.json');
import { builtinThemes, lightTheme, applyTheme } from './theme';
if (localStorage.getItem('theme') == null) {
applyTheme(lightTheme);
@@ -97,15 +96,15 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
return s.device.darkmode;
}, v => {
const themes = os.store.state.device.themes.concat(builtinThemes);
const dark = themes.find(t => t.meta.id == os.store.state.device.darkTheme);
const light = themes.find(t => t.meta.id == os.store.state.device.lightTheme);
const dark = themes.find(t => t.id == os.store.state.device.darkTheme);
const light = themes.find(t => t.id == os.store.state.device.lightTheme);
applyTheme(v ? dark : light);
});
os.store.watch(s => {
return s.device.lightTheme;
}, v => {
const themes = os.store.state.device.themes.concat(builtinThemes);
const theme = themes.find(t => t.meta.id == v);
const theme = themes.find(t => t.id == v);
if (!os.store.state.device.darkmode) {
applyTheme(theme);
}
@@ -114,7 +113,7 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
return s.device.darkTheme;
}, v => {
const themes = os.store.state.device.themes.concat(builtinThemes);
const theme = themes.find(t => t.meta.id == v);
const theme = themes.find(t => t.id == v);
if (os.store.state.device.darkmode) {
applyTheme(theme);
}

View File

@@ -5,7 +5,8 @@
:disabled="wait"
>
<template v-if="!wait">
<template v-if="u.hasPendingFollowRequestFromYou">%fa:hourglass-half% %i18n:@request-pending%</template>
<template v-if="u.hasPendingFollowRequestFromYou && u.isLocked">%fa:hourglass-half% %i18n:@request-pending%</template>
<template v-else-if="u.hasPendingFollowRequestFromYou && !u.isLocked">%fa:hourglass-start% %i18n:@follow-processing%</template>
<template v-else-if="u.isFollowing">%fa:minus% %i18n:@following%</template>
<template v-else-if="!u.isFollowing && u.isLocked">%fa:plus% %i18n:@follow-request%</template>
<template v-else-if="!u.isFollowing && !u.isLocked">%fa:plus% %i18n:@follow%</template>

View File

@@ -44,7 +44,6 @@ export default Vue.extend({
},
mounted() {
document.title = `${(this as any).os.instanceName} Drive`;
document.documentElement.style.background = '#fff';
},
beforeDestroy() {
window.removeEventListener('popstate', this.onPopState);

View File

@@ -11,7 +11,6 @@ import Vue from 'vue';
export default Vue.extend({
mounted() {
document.title = `${(this as any).os.instanceName} %i18n:@reversi%`;
document.documentElement.style.background = '#fff';
},
methods: {
nav(game, actualNav) {

View File

@@ -1,27 +1,40 @@
import * as tinycolor from 'tinycolor2';
type Theme = {
meta: {
id: string;
name: string;
author: string;
base?: string;
vars: any;
};
} & {
[key: string]: string;
export type Theme = {
id: string;
name: string;
author: string;
desc?: string;
base?: 'dark' | 'light';
vars: { [key: string]: string };
props: { [key: string]: string };
};
export const lightTheme: Theme = require('../theme/light.json5');
export const darkTheme: Theme = require('../theme/dark.json5');
export const pinkTheme: Theme = require('../theme/pink.json5');
export const halloweenTheme: Theme = require('../theme/halloween.json5');
export const builtinThemes = [
lightTheme,
darkTheme,
pinkTheme,
halloweenTheme
];
export function applyTheme(theme: Theme, persisted = true) {
if (theme.meta.base) {
const base = [lightTheme, darkTheme].find(x => x.meta.id == theme.meta.base);
theme = Object.assign({}, base, theme);
// Deep copy
const _theme = JSON.parse(JSON.stringify(theme));
if (_theme.base) {
const base = [lightTheme, darkTheme].find(x => x.id == _theme.base);
_theme.vars = Object.assign({}, base.vars, _theme.vars);
_theme.props = Object.assign({}, base.props, _theme.props);
}
const props = compile(theme);
const props = compile(_theme);
Object.entries(props).forEach(([k, v]) => {
if (k == 'meta') return;
document.documentElement.style.setProperty(`--${k}`, v.toString());
});
@@ -34,10 +47,10 @@ function compile(theme: Theme): { [key: string]: string } {
function getColor(code: string): tinycolor.Instance {
// ref
if (code[0] == '@') {
return getColor(theme[code.substr(1)]);
return getColor(theme.props[code.substr(1)]);
}
if (code[0] == '$') {
return getColor(theme.meta.vars[code.substr(1)]);
return getColor(theme.vars[code.substr(1)]);
}
// func
@@ -59,8 +72,7 @@ function compile(theme: Theme): { [key: string]: string } {
const props = {};
Object.entries(theme).forEach(([k, v]) => {
if (k == 'meta') return;
Object.entries(theme.props).forEach(([k, v]) => {
const c = getColor(v);
props[k] = genValue(c);
});
@@ -88,15 +100,3 @@ function compile(theme: Theme): { [key: string]: string } {
function genValue(c: tinycolor.Instance): string {
return c.toRgbString();
}
export const lightTheme = require('../theme/light.json');
export const darkTheme = require('../theme/dark.json');
export const pinkTheme = require('../theme/pink.json');
export const halloweenTheme = require('../theme/halloween.json');
export const builtinThemes = [
lightTheme,
darkTheme,
pinkTheme,
halloweenTheme
];

View File

@@ -1,204 +0,0 @@
{
"meta": {
"id": "dark",
"name": "Dark",
"author": "syuilo",
"vars": {
"primary": "#fb4e4e",
"secondary": "#282C37",
"text": "#d6dae0"
}
},
"primary": "$primary",
"primaryForeground": "#fff",
"secondary": "$secondary",
"bg": ":darken<8<$secondary",
"text": "$text",
"scrollbarTrack": ":darken<5<$secondary",
"scrollbarHandle": ":lighten<5<$secondary",
"scrollbarHandleHover": ":lighten<10<$secondary",
"face": "$secondary",
"faceText": "#fff",
"faceHeader": ":lighten<5<$secondary",
"faceHeaderText": "#e3e5e8",
"faceDivider": "rgba(0, 0, 0, 0.3)",
"faceTextButton": "#9baec8",
"faceTextButtonHover": "#b2c1d5",
"faceTextButtonActive": "#b2c1d5",
"faceClearButtonHover": "rgba(0, 0, 0, 0.1)",
"faceClearButtonActive": "rgba(0, 0, 0, 0.2)",
"popupBg": ":lighten<5<$secondary",
"popupFg": "#d6dce2",
"subNoteBg": "rgba(0, 0, 0, 0.18)",
"subNoteText": ":alpha<0.7<$text",
"renoteGradient": "#314027",
"renoteText": "#9dbb00",
"quoteBorder": "#4e945e",
"noteText": "#fff",
"noteHeaderName": "#fff",
"noteHeaderBadgeFg": "#758188",
"noteHeaderBadgeBg": "rgba(0, 0, 0, 0.25)",
"noteHeaderAdminFg": "#f15f71",
"noteHeaderAdminBg": "#5d282e",
"noteHeaderAcct": "#606984",
"noteHeaderInfo": "#606984",
"noteActions": "#606984",
"noteActionsHover": "#a1a8bf",
"noteActionsReplyHover": "#0af",
"noteActionsRenoteHover": "#8d0",
"noteActionsReactionHover": "#fa0",
"noteActionsHighlighted": "#707b97",
"noteAttachedFile": "rgba(255, 255, 255, 0.1)",
"modalBackdrop": "rgba(0, 0, 0, 0.5)",
"dateDividerBg": ":darken<2<$secondary",
"dateDividerFg": ":alpha<0.7<$text",
"switchTrack": "rgba(255, 255, 255, 0.15)",
"radioBorder": "rgba(255, 255, 255, 0.6)",
"inputBorder": "rgba(255, 255, 255, 0.7)",
"inputLabel": "rgba(255, 255, 255, 0.7)",
"inputText": "#fff",
"buttonBg": "rgba(255, 255, 255, 0.05)",
"buttonHoverBg": "rgba(255, 255, 255, 0.1)",
"buttonActiveBg": "rgba(255, 255, 255, 0.15)",
"autocompleteItemHoverBg": "rgba(255, 255, 255, 0.1)",
"autocompleteItemText": "rgba(255, 255, 255, 0.8)",
"autocompleteItemTextSub": "rgba(255, 255, 255, 0.3)",
"cwButtonBg": "#687390",
"cwButtonFg": "#393f4f",
"cwButtonHoverBg": "#707b97",
"reactionPickerButtonHoverBg": "rgba(255, 255, 255, 0.18)",
"reactionViewerBorder": "rgba(255, 255, 255, 0.1)",
"pollEditorInputBg": "rgba(0, 0, 0, 0.25)",
"pollChoiceText": "#fff",
"pollChoiceBorder": "rgba(255, 255, 255, 0.1)",
"urlPreviewBorder": "rgba(0, 0, 0, 0.4)",
"urlPreviewBorderHover": "rgba(255, 255, 255, 0.2)",
"urlPreviewTitle": "$text",
"urlPreviewText": ":alpha<0.7<$text",
"urlPreviewInfo": ":alpha<0.8<$text",
"calendarWeek": "#43d5dc",
"calendarSaturdayOrSunday": "#ff6679",
"calendarDay": "#c5ced6",
"materBg": "rgba(0, 0, 0, 0.3)",
"chartCaption": ":alpha<0.6<$text",
"announcementsBg": "#253a50",
"announcementsTitle": "#539eff",
"announcementsText": "#fff",
"donationBg": "#5d5242",
"donationFg": "#e4dbce",
"googleSearchBg": "rgba(0, 0, 0, 0.2)",
"googleSearchFg": "#dee4e8",
"googleSearchBorder": "rgba(255, 255, 255, 0.2)",
"googleSearchHoverBorder": "rgba(255, 255, 255, 0.3)",
"googleSearchHoverButton": "rgba(255, 255, 255, 0.1)",
"mfmTitleBg": "rgba(0, 0, 0, 0.2)",
"mfmQuote": ":alpha<0.7<$text",
"mfmQuoteLine": ":alpha<0.6<$text",
"suspendedInfoBg": "#611d1d",
"suspendedInfoFg": "#ffb4b4",
"remoteInfoBg": "#42321c",
"remoteInfoFg": "#ffbd3e",
"messagingRoomBg": "@bg",
"messagingRoomInfo": "#fff",
"messagingRoomDateDividerLine": "rgba(255, 255, 255, 0.1)",
"messagingRoomDateDividerText": "rgba(255, 255, 255, 0.3)",
"messagingRoomMessageInfo": "rgba(255, 255, 255, 0.4)",
"messagingRoomMessageBg": "$secondary",
"messagingRoomMessageFg": "#fff",
"formButtonBorder": "rgba(255, 255, 255, 0.1)",
"formButtonHoverBg": ":alpha<0.2<$primary",
"formButtonHoverBorder": ":alpha<0.5<$primary",
"formButtonActiveBg": ":alpha<0.12<$primary",
"desktopHeaderBg": ":lighten<5<$secondary",
"desktopHeaderFg": "$text",
"desktopHeaderHoverFg": "#fff",
"desktopHeaderSearchBg": "rgba(0, 0, 0, 0.1)",
"desktopHeaderSearchHoverBg": "rgba(255, 255, 255, 0.04)",
"desktopHeaderSearchFg": "#fff",
"desktopNotificationBg": ":alpha<0.9<$secondary",
"desktopNotificationFg": ":alpha<0.7<$text",
"desktopNotificationShadow": "rgba(0, 0, 0, 0.4)",
"desktopPostFormBg": "@face",
"desktopPostFormTextareaBg": "rgba(0, 0, 0, 0.25)",
"desktopPostFormTextareaFg": "#fff",
"desktopPostFormTransparentButtonFg": "$primary",
"desktopPostFormTransparentButtonActiveGradientStart": ":darken<8<$secondary",
"desktopPostFormTransparentButtonActiveGradientEnd": ":darken<3<$secondary",
"desktopRenoteFormFooter": ":lighten<5<$secondary",
"desktopTimelineHeaderShadow": "rgba(0, 0, 0, 0.15)",
"desktopTimelineSrc": "@faceTextButton",
"desktopTimelineSrcHover": "@faceTextButtonHover",
"desktopWindowTitle": "@faceHeaderText",
"desktopWindowShadow": "rgba(0, 0, 0, 0.5)",
"desktopDriveBg": "@bg",
"desktopDriveFolderBg": ":alpha<0.2<$primary",
"desktopDriveFolderHoverBg": ":alpha<0.3<$primary",
"desktopDriveFolderActiveBg": ":alpha<0.3<:darken<10<$primary",
"desktopDriveFolderFg": "#fff",
"desktopSettingsNavItem": ":alpha<0.8<$text",
"desktopSettingsNavItemHover": ":lighten<10<$text",
"deckAcrylicColumnBg": "rgba(0, 0, 0, 0.25)",
"mobileHeaderBg": ":lighten<5<$secondary",
"mobileHeaderFg": "$text",
"mobileNavBackdrop": "rgba(0, 0, 0, 0.7)",
"mobilePostFormDivider": "rgba(0, 0, 0, 0.2)",
"mobilePostFormTextareaBg": "rgba(0, 0, 0, 0.3)",
"mobileDriveNavBg": ":alpha<0.75<$secondary",
"mobileHomeTlItemHover": "rgba(255, 255, 255, 0.1)",
"mobileUserPageName": "#fff",
"mobileUserPageAcct": "$text",
"mobileUserPageDescription": "$text",
"mobileUserPageFollowedBg": "rgba(0, 0, 0, 0.3)",
"mobileUserPageFollowedFg": "$text",
"mobileUserPageStatusHighlight": "#fff",
"mobileUserPageHeaderShadow": "rgba(0, 0, 0, 0.3)",
"mobileAnnouncement": "rgba(30, 129, 216, 0.2)",
"mobileAnnouncementFg": "#fff",
"mobileSignedInAsBg": "#273c34",
"mobileSignedInAsFg": "#49ab63",
"mobileSignoutBg": "#652222",
"mobileSignoutFg": "#ff5f56",
"reversiBannerGradientStart": "#45730e",
"reversiBannerGradientEnd": "#464300",
"reversiDescBg": "rgba(255, 255, 255, 0.1)",
"reversiListItemShadow": "rgba(0, 0, 0, 0.7)",
"reversiMapSelectBorder": "rgba(255, 255, 255, 0.1)",
"reversiMapSelectHoverBorder": "rgba(255, 255, 255, 0.2)",
"reversiRoomFormShadow": "rgba(0, 0, 0, 0.7)",
"reversiRoomFooterBg": ":alpha<0.9<$secondary",
"reversiGameHeaderLine": ":alpha<0.5<$secondary",
"reversiGameEmptyCell": ":lighten<2<$secondary",
"reversiGameEmptyCellMyTurn": ":lighten<5<$secondary",
"reversiGameEmptyCellCanPut": ":lighten<4<$secondary"
}

207
src/client/theme/dark.json5 Normal file
View File

@@ -0,0 +1,207 @@
{
id: 'dark',
name: 'Dark',
author: 'syuilo',
desc: 'Default dark theme',
vars: {
primary: '#fb4e4e',
secondary: '#282C37',
text: '#d6dae0',
},
props: {
primary: '$primary',
primaryForeground: '#fff',
secondary: '$secondary',
bg: ':darken<8<$secondary',
text: '$text',
scrollbarTrack: ':darken<5<$secondary',
scrollbarHandle: ':lighten<5<$secondary',
scrollbarHandleHover: ':lighten<10<$secondary',
face: '$secondary',
faceText: '#fff',
faceHeader: ':lighten<5<$secondary',
faceHeaderText: '#e3e5e8',
faceDivider: 'rgba(0, 0, 0, 0.3)',
faceTextButton: '$text',
faceTextButtonHover: ':lighten<10<$text',
faceTextButtonActive: ':darken<10<$text',
faceClearButtonHover: 'rgba(0, 0, 0, 0.1)',
faceClearButtonActive: 'rgba(0, 0, 0, 0.2)',
popupBg: ':lighten<5<$secondary',
popupFg: '#d6dce2',
subNoteBg: 'rgba(0, 0, 0, 0.18)',
subNoteText: ':alpha<0.7<$text',
renoteGradient: '#314027',
renoteText: '#9dbb00',
quoteBorder: '#4e945e',
noteText: '#fff',
noteHeaderName: '#fff',
noteHeaderBadgeFg: '#758188',
noteHeaderBadgeBg: 'rgba(0, 0, 0, 0.25)',
noteHeaderAdminFg: '#f15f71',
noteHeaderAdminBg: '#5d282e',
noteHeaderAcct: '#606984',
noteHeaderInfo: '#606984',
noteActions: '#606984',
noteActionsHover: '#a1a8bf',
noteActionsReplyHover: '#0af',
noteActionsRenoteHover: '#8d0',
noteActionsReactionHover: '#fa0',
noteActionsHighlighted: '#707b97',
noteAttachedFile: 'rgba(255, 255, 255, 0.1)',
modalBackdrop: 'rgba(0, 0, 0, 0.5)',
dateDividerBg: ':darken<2<$secondary',
dateDividerFg: ':alpha<0.7<$text',
switchTrack: 'rgba(255, 255, 255, 0.15)',
radioBorder: 'rgba(255, 255, 255, 0.6)',
inputBorder: 'rgba(255, 255, 255, 0.7)',
inputLabel: 'rgba(255, 255, 255, 0.7)',
inputText: '#fff',
buttonBg: 'rgba(255, 255, 255, 0.05)',
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
buttonActiveBg: 'rgba(255, 255, 255, 0.15)',
autocompleteItemHoverBg: 'rgba(255, 255, 255, 0.1)',
autocompleteItemText: 'rgba(255, 255, 255, 0.8)',
autocompleteItemTextSub: 'rgba(255, 255, 255, 0.3)',
cwButtonBg: '#687390',
cwButtonFg: '#393f4f',
cwButtonHoverBg: '#707b97',
reactionPickerButtonHoverBg: 'rgba(255, 255, 255, 0.18)',
reactionViewerBorder: 'rgba(255, 255, 255, 0.1)',
pollEditorInputBg: 'rgba(0, 0, 0, 0.25)',
pollChoiceText: '#fff',
pollChoiceBorder: 'rgba(255, 255, 255, 0.1)',
urlPreviewBorder: 'rgba(0, 0, 0, 0.4)',
urlPreviewBorderHover: 'rgba(255, 255, 255, 0.2)',
urlPreviewTitle: '$text',
urlPreviewText: ':alpha<0.7<$text',
urlPreviewInfo: ':alpha<0.8<$text',
calendarWeek: '#43d5dc',
calendarSaturdayOrSunday: '#ff6679',
calendarDay: '#c5ced6',
materBg: 'rgba(0, 0, 0, 0.3)',
chartCaption: ':alpha<0.6<$text',
announcementsBg: '#253a50',
announcementsTitle: '#539eff',
announcementsText: '#fff',
donationBg: '#5d5242',
donationFg: '#e4dbce',
googleSearchBg: 'rgba(0, 0, 0, 0.2)',
googleSearchFg: '#dee4e8',
googleSearchBorder: 'rgba(255, 255, 255, 0.2)',
googleSearchHoverBorder: 'rgba(255, 255, 255, 0.3)',
googleSearchHoverButton: 'rgba(255, 255, 255, 0.1)',
mfmTitleBg: 'rgba(0, 0, 0, 0.2)',
mfmQuote: ':alpha<0.7<$text',
mfmQuoteLine: ':alpha<0.6<$text',
suspendedInfoBg: '#611d1d',
suspendedInfoFg: '#ffb4b4',
remoteInfoBg: '#42321c',
remoteInfoFg: '#ffbd3e',
messagingRoomBg: '@bg',
messagingRoomInfo: '#fff',
messagingRoomDateDividerLine: 'rgba(255, 255, 255, 0.1)',
messagingRoomDateDividerText: 'rgba(255, 255, 255, 0.3)',
messagingRoomMessageInfo: 'rgba(255, 255, 255, 0.4)',
messagingRoomMessageBg: '$secondary',
messagingRoomMessageFg: '#fff',
formButtonBorder: 'rgba(255, 255, 255, 0.1)',
formButtonHoverBg: ':alpha<0.2<$primary',
formButtonHoverBorder: ':alpha<0.5<$primary',
formButtonActiveBg: ':alpha<0.12<$primary',
desktopHeaderBg: ':lighten<5<$secondary',
desktopHeaderFg: '$text',
desktopHeaderHoverFg: '#fff',
desktopHeaderSearchBg: 'rgba(0, 0, 0, 0.1)',
desktopHeaderSearchHoverBg: 'rgba(255, 255, 255, 0.04)',
desktopHeaderSearchFg: '#fff',
desktopNotificationBg: ':alpha<0.9<$secondary',
desktopNotificationFg: ':alpha<0.7<$text',
desktopNotificationShadow: 'rgba(0, 0, 0, 0.4)',
desktopPostFormBg: '@face',
desktopPostFormTextareaBg: 'rgba(0, 0, 0, 0.25)',
desktopPostFormTextareaFg: '#fff',
desktopPostFormTransparentButtonFg: '$primary',
desktopPostFormTransparentButtonActiveGradientStart: ':darken<8<$secondary',
desktopPostFormTransparentButtonActiveGradientEnd: ':darken<3<$secondary',
desktopRenoteFormFooter: ':lighten<5<$secondary',
desktopTimelineHeaderShadow: 'rgba(0, 0, 0, 0.15)',
desktopTimelineSrc: '@faceTextButton',
desktopTimelineSrcHover: '@faceTextButtonHover',
desktopWindowTitle: '@faceHeaderText',
desktopWindowShadow: 'rgba(0, 0, 0, 0.5)',
desktopDriveBg: '@bg',
desktopDriveFolderBg: ':alpha<0.2<$primary',
desktopDriveFolderHoverBg: ':alpha<0.3<$primary',
desktopDriveFolderActiveBg: ':alpha<0.3<:darken<10<$primary',
desktopDriveFolderFg: '#fff',
desktopSettingsNavItem: ':alpha<0.8<$text',
desktopSettingsNavItemHover: ':lighten<10<$text',
deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.25)',
mobileHeaderBg: ':lighten<5<$secondary',
mobileHeaderFg: '$text',
mobileNavBackdrop: 'rgba(0, 0, 0, 0.7)',
mobilePostFormDivider: 'rgba(0, 0, 0, 0.2)',
mobilePostFormTextareaBg: 'rgba(0, 0, 0, 0.3)',
mobileDriveNavBg: ':alpha<0.75<$secondary',
mobileHomeTlItemHover: 'rgba(255, 255, 255, 0.1)',
mobileUserPageName: '#fff',
mobileUserPageAcct: '$text',
mobileUserPageDescription: '$text',
mobileUserPageFollowedBg: 'rgba(0, 0, 0, 0.3)',
mobileUserPageFollowedFg: '$text',
mobileUserPageStatusHighlight: '#fff',
mobileUserPageHeaderShadow: 'rgba(0, 0, 0, 0.3)',
mobileAnnouncement: 'rgba(30, 129, 216, 0.2)',
mobileAnnouncementFg: '#fff',
mobileSignedInAsBg: '#273c34',
mobileSignedInAsFg: '#49ab63',
mobileSignoutBg: '#652222',
mobileSignoutFg: '#ff5f56',
reversiBannerGradientStart: '#45730e',
reversiBannerGradientEnd: '#464300',
reversiDescBg: 'rgba(255, 255, 255, 0.1)',
reversiListItemShadow: 'rgba(0, 0, 0, 0.7)',
reversiMapSelectBorder: 'rgba(255, 255, 255, 0.1)',
reversiMapSelectHoverBorder: 'rgba(255, 255, 255, 0.2)',
reversiRoomFormShadow: 'rgba(0, 0, 0, 0.7)',
reversiRoomFooterBg: ':alpha<0.9<$secondary',
reversiGameHeaderLine: ':alpha<0.5<$secondary',
reversiGameEmptyCell: ':lighten<2<$secondary',
reversiGameEmptyCellMyTurn: ':lighten<5<$secondary',
reversiGameEmptyCellCanPut: ':lighten<4<$secondary',
},
}

View File

@@ -1,17 +0,0 @@
{
"meta": {
"id": "42e4f09b-67d5-498c-af7d-29faa54745b0",
"name": "Halloween",
"author": "syuilo",
"base": "dark",
"vars": {
"primary": "#d67036",
"secondary": "#1f1d30",
"text": "#b1bee3"
}
},
"renoteGradient": "#5d2d1a",
"renoteText": "#ff6c00",
"quoteBorder": "#c3631c"
}

View File

@@ -0,0 +1,21 @@
{
id: '42e4f09b-67d5-498c-af7d-29faa54745b0',
name: 'Halloween',
author: 'syuilo',
desc: 'Hello, Happy Halloween!',
base: 'dark',
vars: {
primary: '#d67036',
secondary: '#1f1d30',
text: '#b1bee3',
},
props: {
renoteGradient: '#5d2d1a',
renoteText: '#ff6c00',
quoteBorder: '#c3631c',
},
}

View File

@@ -1,204 +0,0 @@
{
"meta": {
"id": "light",
"name": "Light",
"author": "syuilo",
"vars": {
"primary": "#fb4e4e",
"secondary": "#fff",
"text": "#666"
}
},
"primary": "$primary",
"primaryForeground": "#fff",
"secondary": "$secondary",
"bg": ":darken<8<$secondary",
"text": "$text",
"scrollbarTrack": "#fff",
"scrollbarHandle": "#00000033",
"scrollbarHandleHover": "#00000066",
"face": "$secondary",
"faceText": "#444",
"faceHeader": ":lighten<5<$secondary",
"faceHeaderText": "#888",
"faceDivider": "rgba(0, 0, 0, 0.082)",
"faceTextButton": "#ccc",
"faceTextButtonHover": "#aaa",
"faceTextButtonActive": "#999",
"faceClearButtonHover": "rgba(0, 0, 0, 0.025)",
"faceClearButtonActive": "rgba(0, 0, 0, 0.05)",
"popupBg": ":lighten<5<$secondary",
"popupFg": "#586069",
"subNoteBg": "rgba(0, 0, 0, 0.01)",
"subNoteText": ":alpha<0.7<$text",
"renoteGradient": "#edfde2",
"renoteText": "#9dbb00",
"quoteBorder": "#c0dac6",
"noteText": "#717171",
"noteHeaderName": ":darken<2<$text",
"noteHeaderBadgeFg": "#aaa",
"noteHeaderBadgeBg": "rgba(0, 0, 0, 0.05)",
"noteHeaderAdminFg": "#f15f71",
"noteHeaderAdminBg": "#ffdfdf",
"noteHeaderAcct": ":alpha<0.7<@noteHeaderName",
"noteHeaderInfo": ":alpha<0.7<@noteHeaderName",
"noteActions": ":alpha<0.3<$text",
"noteActionsHover": ":alpha<0.9<$text",
"noteActionsReplyHover": "#0af",
"noteActionsRenoteHover": "#8d0",
"noteActionsReactionHover": "#fa0",
"noteActionsHighlighted": "#888",
"noteAttachedFile": "rgba(0, 0, 0, 0.05)",
"modalBackdrop": "rgba(0, 0, 0, 0.1)",
"dateDividerBg": ":darken<2<$secondary",
"dateDividerFg": ":alpha<0.7<$text",
"switchTrack": "rgba(0, 0, 0, 0.25)",
"radioBorder": "rgba(0, 0, 0, 0.4)",
"inputBorder": "rgba(0, 0, 0, 0.42)",
"inputLabel": "rgba(0, 0, 0, 0.54)",
"inputText": "#000",
"buttonBg": "rgba(0, 0, 0, 0.05)",
"buttonHoverBg": "rgba(0, 0, 0, 0.1)",
"buttonActiveBg": "rgba(0, 0, 0, 0.15)",
"autocompleteItemHoverBg": "rgba(0, 0, 0, 0.1)",
"autocompleteItemText": "rgba(0, 0, 0, 0.8)",
"autocompleteItemTextSub": "rgba(0, 0, 0, 0.3)",
"cwButtonBg": "#b1b9c1",
"cwButtonFg": "#fff",
"cwButtonHoverBg": "#bbc4ce",
"reactionPickerButtonHoverBg": "#eee",
"reactionViewerBorder": "rgba(0, 0, 0, 0.1)",
"pollEditorInputBg": "#fff",
"pollChoiceText": "#000",
"pollChoiceBorder": "rgba(0, 0, 0, 0.1)",
"urlPreviewBorder": "rgba(0, 0, 0, 0.1)",
"urlPreviewBorderHover": "rgba(0, 0, 0, 0.2)",
"urlPreviewTitle": "$text",
"urlPreviewText": ":alpha<0.7<$text",
"urlPreviewInfo": ":alpha<0.8<$text",
"calendarWeek": "#19a2a9",
"calendarSaturdayOrSunday": "#ef95a0",
"calendarDay": "#777",
"materBg": "rgba(0, 0, 0, 0.1)",
"chartCaption": ":alpha<0.6<$text",
"announcementsBg": "#f3f9ff",
"announcementsTitle": "#4078c0",
"announcementsText": "#57616f",
"donationBg": "#fbead4",
"donationFg": "#777d71",
"googleSearchBg": "#fff",
"googleSearchFg": "#55595c",
"googleSearchBorder": "rgba(0, 0, 0, 0.2)",
"googleSearchHoverBorder": "rgba(0, 0, 0, 0.3)",
"googleSearchHoverButton": "rgba(0, 0, 0, 0.05)",
"mfmTitleBg": "rgba(0, 0, 0, 0.07)",
"mfmQuote": ":alpha<0.6<$text",
"mfmQuoteLine": ":alpha<0.5<$text",
"suspendedInfoBg": "#ffdbdb",
"suspendedInfoFg": "#570808",
"remoteInfoBg": "#fff0db",
"remoteInfoFg": "#573c08",
"messagingRoomBg": "#fff",
"messagingRoomInfo": "#000",
"messagingRoomDateDividerLine": "rgba(0, 0, 0, 0.1)",
"messagingRoomDateDividerText": "rgba(0, 0, 0, 0.3)",
"messagingRoomMessageInfo": "rgba(0, 0, 0, 0.4)",
"messagingRoomMessageBg": "#eee",
"messagingRoomMessageFg": "#333",
"formButtonBorder": "rgba(0, 0, 0, 0.1)",
"formButtonHoverBg": ":alpha<0.12<$primary",
"formButtonHoverBorder": ":alpha<0.3<$primary",
"formButtonActiveBg": ":alpha<0.12<$primary",
"desktopHeaderBg": ":lighten<5<$secondary",
"desktopHeaderFg": "$text",
"desktopHeaderHoverFg": "#7b8c88",
"desktopHeaderSearchBg": "rgba(0, 0, 0, 0.05)",
"desktopHeaderSearchHoverBg": "rgba(0, 0, 0, 0.08)",
"desktopHeaderSearchFg": "#000",
"desktopNotificationBg": ":alpha<0.9<$secondary",
"desktopNotificationFg": ":alpha<0.7<$text",
"desktopNotificationShadow": "rgba(0, 0, 0, 0.2)",
"desktopPostFormBg": ":lighten<33<$primary",
"desktopPostFormTextareaBg": "#fff",
"desktopPostFormTextareaFg": "#333",
"desktopPostFormTransparentButtonFg": ":alpha<0.5<$primary",
"desktopPostFormTransparentButtonActiveGradientStart": ":lighten<30<$primary",
"desktopPostFormTransparentButtonActiveGradientEnd": ":lighten<33<$primary",
"desktopRenoteFormFooter": ":lighten<33<$primary",
"desktopTimelineHeaderShadow": "rgba(0, 0, 0, 0.08)",
"desktopTimelineSrc": "#6f7477",
"desktopTimelineSrcHover": "#525a5f",
"desktopWindowTitle": "#666",
"desktopWindowShadow": "rgba(0, 0, 0, 0.2)",
"desktopDriveBg": "#fff",
"desktopDriveFolderBg": ":lighten<31<$primary",
"desktopDriveFolderHoverBg": ":lighten<27<$primary",
"desktopDriveFolderActiveBg": ":lighten<25<$primary",
"desktopDriveFolderFg": ":darken<10<$primary",
"desktopSettingsNavItem": ":alpha<0.8<$text",
"desktopSettingsNavItemHover": ":darken<10<$text",
"deckAcrylicColumnBg": "rgba(0, 0, 0, 0.1)",
"mobileHeaderBg": ":lighten<5<$secondary",
"mobileHeaderFg": "$text",
"mobileNavBackdrop": "rgba(0, 0, 0, 0.2)",
"mobilePostFormDivider": "rgba(0, 0, 0, 0.1)",
"mobilePostFormTextareaBg": "#fff",
"mobileDriveNavBg": ":alpha<0.75<$secondary",
"mobileHomeTlItemHover": "rgba(0, 0, 0, 0.05)",
"mobileUserPageName": "#757c82",
"mobileUserPageAcct": "#969ea5",
"mobileUserPageDescription": "#757c82",
"mobileUserPageFollowedBg": "#a7bec7",
"mobileUserPageFollowedFg": "#fff",
"mobileUserPageStatusHighlight": "#787e86",
"mobileUserPageHeaderShadow": "rgba(0, 0, 0, 0.07)",
"mobileAnnouncement": "rgba(155, 196, 232, 0.2)",
"mobileAnnouncementFg": "#3f4967",
"mobileSignedInAsBg": "#fcfff5",
"mobileSignedInAsFg": "#2c662d",
"mobileSignoutBg": "#fff6f5",
"mobileSignoutFg": "#cc2727",
"reversiBannerGradientStart": "#8bca3e",
"reversiBannerGradientEnd": "#d6cf31",
"reversiDescBg": "rgba(0, 0, 0, 0.1)",
"reversiListItemShadow": "rgba(0, 0, 0, 0.15)",
"reversiMapSelectBorder": "rgba(0, 0, 0, 0.1)",
"reversiMapSelectHoverBorder": "rgba(0, 0, 0, 0.2)",
"reversiRoomFormShadow": "rgba(0, 0, 0, 0.1)",
"reversiRoomFooterBg": ":alpha<0.9<$secondary",
"reversiGameHeaderLine": "#c4cdd4",
"reversiGameEmptyCell": "rgba(0, 0, 0, 0.06)",
"reversiGameEmptyCellMyTurn": "rgba(0, 0, 0, 0.12)",
"reversiGameEmptyCellCanPut": "rgba(0, 0, 0, 0.9)"
}

View File

@@ -0,0 +1,207 @@
{
id: 'light',
name: 'Light',
author: 'syuilo',
desc: 'Default light theme',
vars: {
primary: '#fb4e4e',
secondary: '#fff',
text: '#666',
},
props: {
primary: '$primary',
primaryForeground: '#fff',
secondary: '$secondary',
bg: ':darken<8<$secondary',
text: '$text',
scrollbarTrack: '#fff',
scrollbarHandle: '#00000033',
scrollbarHandleHover: '#00000066',
face: '$secondary',
faceText: '#444',
faceHeader: ':lighten<5<$secondary',
faceHeaderText: '#888',
faceDivider: 'rgba(0, 0, 0, 0.082)',
faceTextButton: '#ccc',
faceTextButtonHover: '#aaa',
faceTextButtonActive: '#999',
faceClearButtonHover: 'rgba(0, 0, 0, 0.025)',
faceClearButtonActive: 'rgba(0, 0, 0, 0.05)',
popupBg: ':lighten<5<$secondary',
popupFg: '#586069',
subNoteBg: 'rgba(0, 0, 0, 0.01)',
subNoteText: ':alpha<0.7<$text',
renoteGradient: '#edfde2',
renoteText: '#9dbb00',
quoteBorder: '#c0dac6',
noteText: '#717171',
noteHeaderName: ':darken<2<$text',
noteHeaderBadgeFg: '#aaa',
noteHeaderBadgeBg: 'rgba(0, 0, 0, 0.05)',
noteHeaderAdminFg: '#f15f71',
noteHeaderAdminBg: '#ffdfdf',
noteHeaderAcct: ':alpha<0.7<@noteHeaderName',
noteHeaderInfo: ':alpha<0.7<@noteHeaderName',
noteActions: ':alpha<0.3<$text',
noteActionsHover: ':alpha<0.9<$text',
noteActionsReplyHover: '#0af',
noteActionsRenoteHover: '#8d0',
noteActionsReactionHover: '#fa0',
noteActionsHighlighted: '#888',
noteAttachedFile: 'rgba(0, 0, 0, 0.05)',
modalBackdrop: 'rgba(0, 0, 0, 0.1)',
dateDividerBg: ':darken<2<$secondary',
dateDividerFg: ':alpha<0.7<$text',
switchTrack: 'rgba(0, 0, 0, 0.25)',
radioBorder: 'rgba(0, 0, 0, 0.4)',
inputBorder: 'rgba(0, 0, 0, 0.42)',
inputLabel: 'rgba(0, 0, 0, 0.54)',
inputText: '#000',
buttonBg: 'rgba(0, 0, 0, 0.05)',
buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
buttonActiveBg: 'rgba(0, 0, 0, 0.15)',
autocompleteItemHoverBg: 'rgba(0, 0, 0, 0.1)',
autocompleteItemText: 'rgba(0, 0, 0, 0.8)',
autocompleteItemTextSub: 'rgba(0, 0, 0, 0.3)',
cwButtonBg: '#b1b9c1',
cwButtonFg: '#fff',
cwButtonHoverBg: '#bbc4ce',
reactionPickerButtonHoverBg: '#eee',
reactionViewerBorder: 'rgba(0, 0, 0, 0.1)',
pollEditorInputBg: '#fff',
pollChoiceText: '#000',
pollChoiceBorder: 'rgba(0, 0, 0, 0.1)',
urlPreviewBorder: 'rgba(0, 0, 0, 0.1)',
urlPreviewBorderHover: 'rgba(0, 0, 0, 0.2)',
urlPreviewTitle: '$text',
urlPreviewText: ':alpha<0.7<$text',
urlPreviewInfo: ':alpha<0.8<$text',
calendarWeek: '#19a2a9',
calendarSaturdayOrSunday: '#ef95a0',
calendarDay: '#777',
materBg: 'rgba(0, 0, 0, 0.1)',
chartCaption: ':alpha<0.6<$text',
announcementsBg: '#f3f9ff',
announcementsTitle: '#4078c0',
announcementsText: '#57616f',
donationBg: '#fbead4',
donationFg: '#777d71',
googleSearchBg: '#fff',
googleSearchFg: '#55595c',
googleSearchBorder: 'rgba(0, 0, 0, 0.2)',
googleSearchHoverBorder: 'rgba(0, 0, 0, 0.3)',
googleSearchHoverButton: 'rgba(0, 0, 0, 0.05)',
mfmTitleBg: 'rgba(0, 0, 0, 0.07)',
mfmQuote: ':alpha<0.6<$text',
mfmQuoteLine: ':alpha<0.5<$text',
suspendedInfoBg: '#ffdbdb',
suspendedInfoFg: '#570808',
remoteInfoBg: '#fff0db',
remoteInfoFg: '#573c08',
messagingRoomBg: '#fff',
messagingRoomInfo: '#000',
messagingRoomDateDividerLine: 'rgba(0, 0, 0, 0.1)',
messagingRoomDateDividerText: 'rgba(0, 0, 0, 0.3)',
messagingRoomMessageInfo: 'rgba(0, 0, 0, 0.4)',
messagingRoomMessageBg: '#eee',
messagingRoomMessageFg: '#333',
formButtonBorder: 'rgba(0, 0, 0, 0.1)',
formButtonHoverBg: ':alpha<0.12<$primary',
formButtonHoverBorder: ':alpha<0.3<$primary',
formButtonActiveBg: ':alpha<0.12<$primary',
desktopHeaderBg: ':lighten<5<$secondary',
desktopHeaderFg: '$text',
desktopHeaderHoverFg: '#7b8c88',
desktopHeaderSearchBg: 'rgba(0, 0, 0, 0.05)',
desktopHeaderSearchHoverBg: 'rgba(0, 0, 0, 0.08)',
desktopHeaderSearchFg: '#000',
desktopNotificationBg: ':alpha<0.9<$secondary',
desktopNotificationFg: ':alpha<0.7<$text',
desktopNotificationShadow: 'rgba(0, 0, 0, 0.2)',
desktopPostFormBg: ':lighten<33<$primary',
desktopPostFormTextareaBg: '#fff',
desktopPostFormTextareaFg: '#333',
desktopPostFormTransparentButtonFg: ':alpha<0.5<$primary',
desktopPostFormTransparentButtonActiveGradientStart: ':lighten<30<$primary',
desktopPostFormTransparentButtonActiveGradientEnd: ':lighten<33<$primary',
desktopRenoteFormFooter: ':lighten<33<$primary',
desktopTimelineHeaderShadow: 'rgba(0, 0, 0, 0.08)',
desktopTimelineSrc: '#6f7477',
desktopTimelineSrcHover: '#525a5f',
desktopWindowTitle: '#666',
desktopWindowShadow: 'rgba(0, 0, 0, 0.2)',
desktopDriveBg: '#fff',
desktopDriveFolderBg: ':lighten<31<$primary',
desktopDriveFolderHoverBg: ':lighten<27<$primary',
desktopDriveFolderActiveBg: ':lighten<25<$primary',
desktopDriveFolderFg: ':darken<10<$primary',
desktopSettingsNavItem: ':alpha<0.8<$text',
desktopSettingsNavItemHover: ':darken<10<$text',
deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.1)',
mobileHeaderBg: ':lighten<5<$secondary',
mobileHeaderFg: '$text',
mobileNavBackdrop: 'rgba(0, 0, 0, 0.2)',
mobilePostFormDivider: 'rgba(0, 0, 0, 0.1)',
mobilePostFormTextareaBg: '#fff',
mobileDriveNavBg: ':alpha<0.75<$secondary',
mobileHomeTlItemHover: 'rgba(0, 0, 0, 0.05)',
mobileUserPageName: '#757c82',
mobileUserPageAcct: '#969ea5',
mobileUserPageDescription: '#757c82',
mobileUserPageFollowedBg: '#a7bec7',
mobileUserPageFollowedFg: '#fff',
mobileUserPageStatusHighlight: '#787e86',
mobileUserPageHeaderShadow: 'rgba(0, 0, 0, 0.07)',
mobileAnnouncement: 'rgba(155, 196, 232, 0.2)',
mobileAnnouncementFg: '#3f4967',
mobileSignedInAsBg: '#fcfff5',
mobileSignedInAsFg: '#2c662d',
mobileSignoutBg: '#fff6f5',
mobileSignoutFg: '#cc2727',
reversiBannerGradientStart: '#8bca3e',
reversiBannerGradientEnd: '#d6cf31',
reversiDescBg: 'rgba(0, 0, 0, 0.1)',
reversiListItemShadow: 'rgba(0, 0, 0, 0.15)',
reversiMapSelectBorder: 'rgba(0, 0, 0, 0.1)',
reversiMapSelectHoverBorder: 'rgba(0, 0, 0, 0.2)',
reversiRoomFormShadow: 'rgba(0, 0, 0, 0.1)',
reversiRoomFooterBg: ':alpha<0.9<$secondary',
reversiGameHeaderLine: '#c4cdd4',
reversiGameEmptyCell: 'rgba(0, 0, 0, 0.06)',
reversiGameEmptyCellMyTurn: 'rgba(0, 0, 0, 0.12)',
reversiGameEmptyCellCanPut: 'rgba(0, 0, 0, 0.9)',
},
}

View File

@@ -1,17 +0,0 @@
{
"meta": {
"id": "e9c8c01d-9c15-48d0-9b5c-3d00843b5b36",
"name": "Pink",
"author": "syuilo",
"base": "light",
"vars": {
"primary": "rgb(251, 78, 112)",
"secondary": "rgb(255, 218, 240)",
"text": "rgb(113, 91, 102)"
}
},
"renoteGradient": "#ffb1c9",
"renoteText": "#ff588d",
"quoteBorder": "#ff6c9b"
}

View File

@@ -0,0 +1,20 @@
{
id: 'e9c8c01d-9c15-48d0-9b5c-3d00843b5b36',
name: 'Pink',
author: 'syuilo',
base: 'light',
vars: {
primary: 'rgb(251, 78, 112)',
secondary: 'rgb(255, 218, 240)',
text: 'rgb(113, 91, 102)',
},
props: {
renoteGradient: '#ffb1c9',
renoteText: '#ff588d',
quoteBorder: '#ff6c9b',
},
}

View File

@@ -27,21 +27,6 @@ Note.createIndex({
});
export default Note;
// 後方互換性のため
Note.findOne({
fileIds: { $exists: true }
}).then(n => {
if (n == null) {
Note.update({}, {
$rename: {
mediaIds: 'fileIds'
}
}, {
multi: true
});
}
});
export function isValidText(text: string): boolean {
return length(text.trim()) <= 1000 && text.trim() != '';
}

View File

@@ -3,11 +3,6 @@ import db from '../db/mongodb';
const Stats = db.get<IStats>('stats');
// 後方互換性のため
Stats.dropIndex({ date: -1 } as any).catch((e: mongo.MongoError) => {
if (e.code !== 27) throw e;
});
Stats.createIndex({ span: -1, date: -1 }, { unique: true });
export default Stats;

View File

@@ -35,28 +35,6 @@ User.createIndex('uri', { sparse: true, unique: true });
export default User;
// 後方互換性のため
User.findOne({
pinnedNoteId: { $exists: true }
}).then(async x => {
if (x == null) return;
const users = await User.find({
pinnedNoteId: { $exists: true }
});
users.forEach(u => {
User.update({ _id: u._id }, {
$set: {
pinnedNoteIds: [(u as any).pinnedNoteId]
},
$unset: {
pinnedNoteId: ''
}
});
});
});
type IUserBase = {
_id: mongo.ObjectID;
createdAt: Date;
@@ -135,6 +113,7 @@ export interface ILocalUser extends IUserBase {
export interface IRemoteUser extends IUserBase {
inbox: string;
sharedInbox?: string;
featured?: string;
endpoints: string[];
uri: string;
url?: string;

View File

@@ -0,0 +1,22 @@
import { IRemoteUser } from '../../../../models/user';
import { IAdd } from '../../type';
import { resolveNote } from '../../models/note';
import { addPinned } from '../../../../services/i/pin';
export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => {
if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor');
}
if (activity.target == null) {
throw new Error('target is null');
}
if (activity.target === actor.featured) {
const note = await resolveNote(activity.object);
await addPinned(actor, note._id);
return;
}
throw new Error(`unknown target: ${activity.target}`);
};

View File

@@ -8,6 +8,8 @@ import like from './like';
import announce from './announce';
import accept from './accept';
import reject from './reject';
import add from './add';
import remove from './remove';
const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
switch (activity.type) {
@@ -31,6 +33,14 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
await reject(actor, activity);
break;
case 'Add':
await add(actor, activity).catch(err => console.log(err));
break;
case 'Remove':
await remove(actor, activity).catch(err => console.log(err));
break;
case 'Announce':
await announce(actor, activity);
break;

View File

@@ -0,0 +1,22 @@
import { IRemoteUser } from '../../../../models/user';
import { IRemove } from '../../type';
import { resolveNote } from '../../models/note';
import { removePinned } from '../../../../services/i/pin';
export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => {
if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor');
}
if (activity.target == null) {
throw new Error('target is null');
}
if (activity.target === actor.featured) {
const note = await resolveNote(activity.object);
await removePinned(actor, note._id);
return;
}
throw new Error(`unknown target: ${activity.target}`);
};

View File

@@ -56,7 +56,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
log(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ
const actor = await resolvePerson(note.attributedTo) as IRemoteUser;
const actor = await resolvePerson(note.attributedTo, null, resolver) as IRemoteUser;
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
@@ -73,7 +73,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
visibility = 'followers';
} else {
visibility = 'specified';
visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri)));
visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, null, resolver)));
}
}
//#endergion

View File

@@ -3,15 +3,16 @@ import { toUnicode } from 'punycode';
import * as debug from 'debug';
import config from '../../../config';
import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user';
import User, { validateUsername, isValidName, IUser, IRemoteUser, isRemoteUser } from '../../../models/user';
import Resolver from '../resolver';
import { resolveImage } from './image';
import { isCollectionOrOrderedCollection, IPerson } from '../type';
import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type';
import { IDriveFile } from '../../../models/drive-file';
import Meta from '../../../models/meta';
import htmlToMFM from '../../../mfm/html-to-mfm';
import { updateUserStats } from '../../../services/update-chart';
import { URL } from 'url';
import { resolveNote } from './note';
const log = debug('misskey:activitypub');
@@ -155,6 +156,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
},
inbox: person.inbox,
sharedInbox: person.sharedInbox,
featured: person.featured,
endpoints: person.endpoints,
uri: person.id,
url: person.url,
@@ -211,6 +213,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
user.bannerUrl = bannerUrl;
//#endregion
await updateFeatured(user._id).catch(err => console.log(err));
return user;
}
@@ -282,6 +285,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
updatedAt: new Date(),
inbox: person.inbox,
sharedInbox: person.sharedInbox,
featured: person.featured,
avatarId: avatar ? avatar._id : null,
bannerId: banner ? banner._id : null,
avatarUrl: (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : null,
@@ -303,6 +307,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
},
}
});
await updateFeatured(exist._id).catch(err => console.log(err));
}
/**
@@ -311,7 +317,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
* Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/
export async function resolvePerson(uri: string, verifier?: string): Promise<IUser> {
export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<IUser> {
if (typeof uri !== 'string') throw 'uri is not string';
//#region このサーバーに既に登録されていたらそれを返す
@@ -323,5 +329,37 @@ export async function resolvePerson(uri: string, verifier?: string): Promise<IUs
//#endregion
// リモートサーバーからフェッチしてきて登録
return await createPerson(uri);
if (resolver == null) resolver = new Resolver();
return await createPerson(uri, resolver);
}
export async function updateFeatured(userId: mongo.ObjectID) {
const user = await User.findOne({ _id: userId });
if (!isRemoteUser(user)) return;
if (!user.featured) return;
log(`Updating the featured: ${user.uri}`);
const resolver = new Resolver();
// Resolve to (Ordered)Collection Object
const collection = await resolver.resolveCollection(user.featured);
if (!isCollectionOrOrderedCollection(collection)) throw new Error(`Object is not Collection or OrderedCollection`);
// Resolve to Object(may be Note) arrays
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
const items = await resolver.resolve(unresolvedItems);
if (!Array.isArray(items)) throw new Error(`Collection items is not an array`);
// Resolve and regist Notes
const featuredNotes = await Promise.all(items
.filter(item => item.type === 'Note')
.slice(0, 5)
.map(item => resolveNote(item, resolver)));
await User.update({ _id: user._id }, {
$set: {
pinnedNoteIds: featuredNotes.map(note => note._id)
}
});
}

View File

@@ -19,11 +19,11 @@ export default class Resolver {
switch (collection.type) {
case 'Collection':
collection.objects = collection.object.items;
collection.objects = collection.items;
break;
case 'OrderedCollection':
collection.objects = collection.object.orderedItems;
collection.objects = collection.orderedItems;
break;
default:

View File

@@ -91,6 +91,14 @@ export interface IReject extends IActivity {
type: 'Reject';
}
export interface IAdd extends IActivity {
type: 'Add';
}
export interface IRemove extends IActivity {
type: 'Remove';
}
export interface ILike extends IActivity {
type: 'Like';
_misskey_reaction: string;
@@ -109,5 +117,7 @@ export type Object =
IFollow |
IAccept |
IReject |
IAdd |
IRemove |
ILike |
IAnnounce;

View File

@@ -1,8 +1,7 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import User, { ILocalUser } from '../../../../models/user';
import Note from '../../../../models/note';
import { ILocalUser } from '../../../../models/user';
import { pack } from '../../../../models/user';
import { deliverPinnedChange } from '../../../../services/i/pin';
import { addPinned } from '../../../../services/i/pin';
import getParams from '../../get-params';
export const meta = {
@@ -27,41 +26,18 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
const [ps, psErr] = getParams(meta, params);
if (psErr) return rej(psErr);
// Fetch pinee
const note = await Note.findOne({
_id: ps.noteId,
userId: user._id
});
if (note === null) {
return rej('note not found');
// Processing
try {
await addPinned(user, ps.noteId);
} catch (e) {
return rej(e.message);
}
const pinnedNoteIds = user.pinnedNoteIds || [];
if (pinnedNoteIds.length > 5) {
return rej('cannot pin more notes');
}
if (pinnedNoteIds.some(id => id.equals(note._id))) {
return rej('already exists');
}
pinnedNoteIds.unshift(note._id);
await User.update(user._id, {
$set: {
pinnedNoteIds: pinnedNoteIds
}
});
// Serialize
const iObj = await pack(user, user, {
detail: true
});
// Send response
res(iObj);
// Send Add to followers
deliverPinnedChange(user._id, note._id, true);
});

View File

@@ -1,8 +1,7 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import User, { ILocalUser } from '../../../../models/user';
import Note from '../../../../models/note';
import { ILocalUser } from '../../../../models/user';
import { pack } from '../../../../models/user';
import { deliverPinnedChange } from '../../../../services/i/pin';
import { removePinned } from '../../../../services/i/pin';
import getParams from '../../get-params';
export const meta = {
@@ -27,31 +26,18 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
const [ps, psErr] = getParams(meta, params);
if (psErr) return rej(psErr);
// Fetch unpinee
const note = await Note.findOne({
_id: ps.noteId,
userId: user._id
});
if (note === null) {
return rej('note not found');
// Processing
try {
await removePinned(user, ps.noteId);
} catch (e) {
return rej(e.message);
}
const pinnedNoteIds = (user.pinnedNoteIds || []).filter(id => !id.equals(note._id));
await User.update(user._id, {
$set: {
pinnedNoteIds: pinnedNoteIds
}
});
// Serialize
const iObj = await pack(user, user, {
detail: true
});
// Send response
res(iObj);
// Send Remove to followers
deliverPinnedChange(user._id, note._id, false);
});

View File

@@ -1,12 +1,83 @@
import config from '../../config';
import * as mongo from 'mongodb';
import User, { isLocalUser, isRemoteUser, ILocalUser } from '../../models/user';
import User, { isLocalUser, isRemoteUser, ILocalUser, IUser } from '../../models/user';
import Note from '../../models/note';
import Following from '../../models/following';
import renderAdd from '../../remote/activitypub/renderer/add';
import renderRemove from '../../remote/activitypub/renderer/remove';
import packAp from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
/**
* 指定した投稿をピン留めします
* @param user
* @param noteId
*/
export async function addPinned(user: IUser, noteId: mongo.ObjectID) {
// Fetch pinee
const note = await Note.findOne({
_id: noteId,
userId: user._id
});
if (note === null) {
throw new Error('note not found');
}
const pinnedNoteIds = user.pinnedNoteIds || [];
if (pinnedNoteIds.length > 5) {
throw new Error('cannot pin more notes');
}
if (pinnedNoteIds.some(id => id.equals(note._id))) {
throw new Error('already exists');
}
pinnedNoteIds.unshift(note._id);
await User.update(user._id, {
$set: {
pinnedNoteIds: pinnedNoteIds
}
});
// Deliver to remote followers
if (isLocalUser(user)) {
deliverPinnedChange(user._id, note._id, true);
}
}
/**
* 指定した投稿のピン留めを解除します
* @param user
* @param noteId
*/
export async function removePinned(user: IUser, noteId: mongo.ObjectID) {
// Fetch unpinee
const note = await Note.findOne({
_id: noteId,
userId: user._id
});
if (note === null) {
throw new Error('note not found');
}
const pinnedNoteIds = (user.pinnedNoteIds || []).filter(id => !id.equals(note._id));
await User.update(user._id, {
$set: {
pinnedNoteIds: pinnedNoteIds
}
});
// Deliver to remote followers
if (isLocalUser(user)) {
deliverPinnedChange(user._id, noteId, false);
}
}
export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo.ObjectID, isAddition: boolean) {
const user = await User.findOne({
_id: userId

View File

@@ -54,20 +54,45 @@ describe('Text', () => {
], tokens2);
});
it('mention', () => {
const tokens = analyze('@himawari お腹ペコい');
assert.deepEqual([
{ type: 'mention', content: '@himawari', username: 'himawari', host: null },
{ type: 'text', content: ' お腹ペコい' }
], tokens);
});
describe('mention', () => {
it('local', () => {
const tokens = analyze('@himawari お腹ペコい');
assert.deepEqual([
{ type: 'mention', content: '@himawari', username: 'himawari', host: null },
{ type: 'text', content: ' お腹ペコい' }
], tokens);
});
it('remote mention', () => {
const tokens = analyze('@hima_sub@namori.net お腹ペコい');
assert.deepEqual([
{ type: 'mention', content: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' },
{ type: 'text', content: ' お腹ペコい' }
], tokens);
it('remote', () => {
const tokens = analyze('@hima_sub@namori.net お腹ペコい');
assert.deepEqual([
{ type: 'mention', content: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' },
{ type: 'text', content: ' お腹ペコい' }
], tokens);
});
/*
it('ignore', () => {
const tokens = analyze('idolm@ster');
assert.deepEqual([
{ type: 'text', content: 'idolm@ster' }
], tokens);
const tokens2 = analyze('@a\n@b\n@c');
assert.deepEqual([
{ type: 'mention', content: '@a', username: 'a', host: null },
{ type: 'text', content: '\n' },
{ type: 'mention', content: '@b', username: 'b', host: null },
{ type: 'text', content: '\n' },
{ type: 'mention', content: '@c', username: 'c', host: null }
], tokens2);
const tokens3 = analyze('**x**@a');
assert.deepEqual([
{ type: 'bold', content: '**x**', bold: 'x' },
{ type: 'mention', content: '@a', username: 'a', host: null }
], tokens3);
});
*/
});
it('hashtag', () => {

View File

@@ -196,6 +196,9 @@ module.exports = {
}, {
test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/,
loader: 'url-loader'
}, {
test: /\.json5$/,
loader: 'json5-loader'
}, {
test: /\.ts$/,
exclude: /node_modules/,