Merge branch 'main' into feat/secure-fetch

This commit is contained in:
Norm 2022-07-30 18:59:24 -04:00
commit 66df12df0a
Signed by: norm
GPG Key ID: 7123E30E441E80DE
20 changed files with 495 additions and 624 deletions

View File

@ -25,6 +25,7 @@ export const AppRepository = db.getRepository(App).extend({
return {
id: app.id,
name: app.name,
description: app.description,
callbackUrl: app.callbackUrl,
permission: app.permission,
...(opts.includeSecret ? { secret: app.secret } : {}),

View File

@ -5,11 +5,10 @@ import { Users, DriveFiles, Notes, Channels, Blockings } from '@/models/index.js
import { DriveFile } from '@/models/entities/drive-file.js';
import { Note } from '@/models/entities/note.js';
import { Channel } from '@/models/entities/channel.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { MAX_NOTE_TEXT_LENGTH, HOUR } from '@/const.js';
import { noteVisibilities } from '../../../../types.js';
import { ApiError } from '../../error.js';
import define from '../../define.js';
import { HOUR } from '@/const.js';
import { getNote } from '../../common/getters.js';
export const meta = {
@ -78,13 +77,24 @@ export const meta = {
code: 'YOU_HAVE_BEEN_BLOCKED',
id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3',
},
lessRestrictiveVisibility: {
message: 'The visibility cannot be less restrictive than the parent note.',
code: 'LESS_RESTRICTIVE_VISIBILITY',
id: 'c8ab7a7a-8852-41e2-8b24-079bbaceb585',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
visibility: { type: 'string', enum: noteVisibilities, default: 'public' },
visibility: {
description: 'The visibility of the new note. Must be the same or more restrictive than a replied to or quoted note.',
type: 'string',
enum: noteVisibilities,
default: 'public',
},
visibleUserIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id',
} },
@ -195,6 +205,11 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.cannotReRenote);
}
// check that the visibility is not less restrictive
if (noteVisibilities.indexOf(renote.visibility) > noteVisibilities.indexOf(ps.visibility)) {
throw new ApiError(meta.errors.lessRestrictiveVisibility);
}
// Check blocking
if (renote.userId !== user.id) {
const block = await Blockings.findOneBy({
@ -219,6 +234,11 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.cannotReplyToPureRenote);
}
// check that the visibility is not less restrictive
if (noteVisibilities.indexOf(reply.visibility) > noteVisibilities.indexOf(ps.visibility)) {
throw new ApiError(meta.errors.lessRestrictiveVisibility);
}
// Check blocking
if (reply.userId !== user.id) {
const block = await Blockings.findOneBy({

View File

@ -170,11 +170,6 @@ export default async (user: { id: User['id']; username: User['username']; host:
data.visibility = 'followers';
}
// 返信対象がpublicではないならhomeにする
if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') {
data.visibility = 'home';
}
// ローカルのみをRenoteしたらローカルのみにする
if (data.renote && data.renote.localOnly && data.channel == null) {
data.localOnly = true;

View File

@ -1,5 +1,8 @@
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const;
/**
* Note visibilities, ordered from most to least open.
*/
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;

View File

@ -26,150 +26,88 @@
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
import { debounce } from 'throttle-debounce';
import MkButton from '@/components/ui/button.vue';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
},
const emit = defineEmits<{
(ev: 'change', v: any): void;
(ev: 'keydown', v: KeyboardEvent): void;
(ev: 'enter'): void;
(ev: 'update:modelValue', v: string): void;
}>();
props: {
modelValue: {
required: true,
},
type: {
type: String,
required: false,
},
required: {
type: Boolean,
required: false,
},
readonly: {
type: Boolean,
required: false,
},
disabled: {
type: Boolean,
required: false,
},
pattern: {
type: String,
required: false,
},
placeholder: {
type: String,
required: false,
},
autofocus: {
type: Boolean,
required: false,
default: false,
},
autocomplete: {
required: false,
},
spellcheck: {
required: false,
},
code: {
type: Boolean,
required: false,
},
tall: {
type: Boolean,
required: false,
default: false,
},
pre: {
type: Boolean,
required: false,
default: false,
},
debounce: {
type: Boolean,
required: false,
default: false,
},
manualSave: {
type: Boolean,
required: false,
default: false,
},
},
const props = withDefaults(defineProps<{
modelValue: string;
type?: string;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
pattern?: string;
placeholder?: string;
autofocus?: boolean;
autocomplete?: boolean;
spellcheck?: boolean;
code?: boolean;
tall?: boolean;
pre?: boolean;
debounce?: boolean;
manualSave?: boolean;
}>(), {
autofocus: false,
tall: false,
pre: false,
manualSave: false,
});
emits: ['change', 'keydown', 'enter', 'update:modelValue'],
const { modelValue, autofocus } = toRefs(props);
// modelValue is read only, so a separate ref is needed.
const v = $ref(modelValue.value);
setup(props, context) {
const { modelValue, autofocus } = toRefs(props);
const v = ref(modelValue.value);
const focused = ref(false);
const changed = ref(false);
const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null);
const inputEl = ref(null);
const focused = $ref(false);
const changed = $ref(false);
const invalid = $ref(false);
const filled = computed(() => modelValue.value !== '' && modelValue.value != null);
const inputEl = $ref(null);
const focus = () => inputEl.value.focus();
const onInput = (ev) => {
changed.value = true;
context.emit('change', ev);
};
const onKeydown = (ev: KeyboardEvent) => {
context.emit('keydown', ev);
const focus = () => inputEl.focus();
const onInput = evt => {
changed = true;
emit('change', evt);
};
const onKeydown = (evt: KeyboardEvent) => {
emit('keydown', evt);
if (evt.code === 'Enter') {
emit('enter');
}
};
const updated = () => {
changed = false;
emit('update:modelValue', v);
};
const debouncedUpdated = debounce(1000, updated);
if (ev.code === 'Enter') {
context.emit('enter');
}
};
watch(modelValue, newValue => {
if (!props.manualSave) {
if (props.debounce) {
debouncedUpdated();
} else {
updated();
}
}
const updated = () => {
changed.value = false;
context.emit('update:modelValue', v.value);
};
invalid = inputEl.validity.badInput;
});
const debouncedUpdated = debounce(1000, updated);
watch(modelValue, newValue => {
v.value = newValue;
});
watch(v, newValue => {
if (!props.manualSave) {
if (props.debounce) {
debouncedUpdated();
} else {
updated();
}
}
invalid.value = inputEl.value.validity.badInput;
});
onMounted(() => {
nextTick(() => {
if (autofocus.value) {
focus();
}
});
});
return {
v,
focused,
invalid,
changed,
filled,
inputEl,
focus,
onInput,
onKeydown,
updated,
};
},
onMounted(() => {
nextTick(() => {
if (props.autofocus) {
inputEl.focus();
}
});
});
</script>

View File

@ -3,7 +3,7 @@
<div class="mjndxjcg">
<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
<p><i class="fas fa-exclamation-triangle"></i> {{ i18n.ts.somethingHappened }}</p>
<MkButton class="button" @click="() => $emit('retry')">{{ i18n.ts.retry }}</MkButton>
<MkButton v-if="!final" class="button" @click="retry">{{ i18n.ts.retry }}</MkButton>
</div>
</transition>
</template>
@ -11,6 +11,16 @@
<script lang="ts" setup>
import MkButton from '@/components/ui/button.vue';
import { i18n } from '@/i18n';
const emit = defineEmits<{
(ev: 'retry'): void;
}>();
const retry = emit.bind(null, 'retry');
defineProps<{
// true if this operation can not be retried
final?: boolean;
}>();
</script>
<style lang="scss" scoped>

View File

@ -1,7 +1,7 @@
<template>
<div class="mk-google">
<div class="mk-search">
<input v-model="query" type="search" :placeholder="q">
<button @click="search"><i class="fas fa-search"></i> {{ $ts.search }}</button>
<button><i class="fas fa-search"></i> {{ $ts.search }}</button>
</div>
</template>
@ -13,14 +13,10 @@ const props = defineProps<{
}>();
const query = ref(props.q);
const search = () => {
window.open(`https://www.google.com/search?q=${query.value}`, '_blank');
};
</script>
<style lang="scss" scoped>
.mk-google {
.mk-search {
display: flex;
margin: 8px 0;

View File

@ -7,7 +7,7 @@ import MkEmoji from '@/components/global/emoji.vue';
import { concat } from '@/scripts/array';
import MkFormula from '@/components/formula.vue';
import MkCode from '@/components/code.vue';
import MkGoogle from '@/components/google.vue';
import MkSearch from '@/components/mfm-search.vue';
import MkSparkle from '@/components/sparkle.vue';
import MkA from '@/components/global/a.vue';
import { host } from '@/config';
@ -306,7 +306,7 @@ export default defineComponent({
}
case 'search': {
return [h(MkGoogle, {
return [h(MkSearch, {
key: Math.random(),
q: token.props.query,
})];

View File

@ -6,13 +6,13 @@
<div v-else-if="typeof value === 'number'" class="number">{{ number(value) }}</div>
<div v-else-if="isArray(value) && isEmpty(value)" class="array empty">[]</div>
<div v-else-if="isArray(value)" class="array">
<div v-for="i in value.length" class="element">
<div v-for="i in value.length" :key="i" class="element">
{{ i }}: <XValue :value="value[i - 1]" collapsed/>
</div>
</div>
<div v-else-if="isObject(value) && isEmpty(value)" class="object empty">{}</div>
<div v-else-if="isObject(value)" class="object">
<div v-for="k in Object.keys(value)" class="kv">
<div v-for="k in Object.keys(value)" :key="k" class="kv">
<button class="toggle _button" :class="{ visible: collapsable(value[k]) }" @click="collapsed[k] = !collapsed[k]">{{ collapsed[k] ? '+' : '-' }}</button>
<div class="k">{{ k }}:</div>
<div v-if="collapsed[k]" class="v">
@ -28,54 +28,38 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, ref } from 'vue';
<script lang="ts" setup>
import { reactive, defineProps } from 'vue';
import number from '@/filters/number';
export default defineComponent({
name: 'XValue',
const props = defineProps<{
value: any;
}>();
props: {
value: {
required: true,
},
},
const collapsed = reactive({});
setup(props) {
const collapsed = reactive({});
if (isObject(props.value)) {
for (const key in props.value) {
collapsed[key] = collapsable(props.value[key]);
}
}
if (isObject(props.value)) {
for (const key in props.value) {
collapsed[key] = collapsable(props.value[key]);
}
}
function isObject(v): boolean {
return typeof v === 'object' && !Array.isArray(v) && v !== null;
}
function isObject(v): boolean {
return typeof v === 'object' && !Array.isArray(v) && v !== null;
}
function isArray(v): boolean {
return Array.isArray(v);
}
function isArray(v): boolean {
return Array.isArray(v);
}
function isEmpty(v): boolean {
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
}
function isEmpty(v): boolean {
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
}
function collapsable(v): boolean {
return (isObject(v) || isArray(v)) && !isEmpty(v);
}
function collapsable(v): boolean {
return (isObject(v) || isArray(v)) && !isEmpty(v);
}
return {
number,
collapsed,
isObject,
isArray,
isEmpty,
collapsable,
};
},
});
</script>
<style lang="scss" scoped>

View File

@ -27,90 +27,70 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { ref } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkSwitch from '@/components/form/switch.vue';
import MkTextarea from '@/components/form/textarea.vue';
import MkRadio from '@/components/form/radio.vue';
import * as os from '@/os';
import * as config from '@/config';
import { $i } from '@/account';
export default defineComponent({
components: {
MkButton,
MkInput,
MkSwitch,
MkTextarea,
MkRadio,
},
const text = ref('');
const flag = ref(true);
const radio = ref('misskey');
const mfm = ref(`Hello world! This is an @example mention. BTW you are @${$i ? $i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`);
data() {
return {
text: '',
flag: true,
radio: 'misskey',
mfm: `Hello world! This is an @example mention. BTW you are @${this.$i ? this.$i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`
};
},
function openDialog(): void {
os.alert({
type: 'warning',
title: 'Oh my Aichan',
text: 'Lorem ipsum dolor sit amet, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
});
}
methods: {
async openDialog() {
os.alert({
type: 'warning',
title: 'Oh my Aichan',
text: 'Lorem ipsum dolor sit amet, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
});
function openForm(): void {
os.form('Example form', {
foo: {
type: 'boolean',
default: true,
label: 'This is a boolean property',
},
async openForm() {
os.form('Example form', {
foo: {
type: 'boolean',
default: true,
label: 'This is a boolean property'
},
bar: {
type: 'number',
default: 300,
label: 'This is a number property'
},
baz: {
type: 'string',
default: 'Misskey makes you happy.',
label: 'This is a string property'
},
});
bar: {
type: 'number',
default: 300,
label: 'This is a number property',
},
async openDrive() {
os.selectDriveFile();
baz: {
type: 'string',
default: 'Misskey makes you happy.',
label: 'This is a string property',
},
});
}
async selectUser() {
os.selectUser();
},
function openDrive(): void {
os.selectDriveFile(true);
}
async openMenu(ev) {
os.popupMenu([{
type: 'label',
text: 'Fruits'
}, {
text: 'Create some apples',
action: () => {},
}, {
text: 'Read some oranges',
action: () => {},
}, {
text: 'Update some melons',
action: () => {},
}, null, {
text: 'Delete some bananas',
danger: true,
action: () => {},
}], ev.currentTarget ?? ev.target);
},
}
});
function openMenu(ev): void {
os.popupMenu([{
type: 'label',
text: 'Fruits'
}, {
text: 'Create some apples',
action: (): void => {},
}, {
text: 'Read some oranges',
action: (): void => {},
}, {
text: 'Update some melons',
action: (): void => {},
}, null, {
text: 'Delete some bananas',
danger: true,
action: (): void => {},
}], ev.currentTarget ?? ev.target);
}
</script>

View File

@ -31,7 +31,7 @@ type ModalTypes = 'popup' | 'dialog' | 'dialog:top' | 'drawer';
const props = withDefaults(defineProps<{
manualShowing?: boolean | null;
anchor?: { x: string; y: string; };
src?: HTMLElement;
src?: HTMLElement | null;
preferType?: ModalTypes | 'auto';
zPriority?: 'low' | 'middle' | 'high';
noOverlap?: boolean;
@ -102,7 +102,7 @@ const MARGIN = 16;
const align = () => {
if (props.src == null) return;
if (type === 'drawer') return;
if (type == 'dialog') return;
if (type === 'dialog') return;
if (content == null) return;

View File

@ -7,7 +7,7 @@
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option>
</MkSelect>
<MkButton inline primary class="mk-widget-add" @click="addWidget"><i class="fas fa-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton>
<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
</header>
<XDraggable
v-model="widgets_"
@ -30,73 +30,57 @@
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { defineComponent, defineAsyncComponent, reactive, ref, computed } from 'vue';
import { v4 as uuid } from 'uuid';
import MkSelect from '@/components/form/select.vue';
import MkButton from '@/components/ui/button.vue';
import { widgets as widgetDefs } from '@/widgets';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
XDraggable: defineAsyncComponent(() => import('vuedraggable')),
MkSelect,
MkButton,
},
const XDraggable = defineAsyncComponent(() => import('vuedraggable'));
props: {
widgets: {
type: Array,
required: true,
},
edit: {
type: Boolean,
required: true,
},
},
type Widgets = any[];
emits: ['updateWidgets', 'addWidget', 'removeWidget', 'updateWidget', 'exit'],
const props = defineProps<{
widgets: Widgets;
edit: boolean;
}>();
setup(props, context) {
const widgetRefs = reactive({});
const configWidget = (id: string) => {
widgetRefs[id].configure();
};
const widgetAdderSelected = ref(null);
const addWidget = () => {
if (widgetAdderSelected.value == null) return;
const emit = defineEmits<{
(ev: 'updateWidgets', v: Widgets): void;
(ev: 'addWidget', v: { name: string; id: string; data: Record<string, any>; }): void;
(ev: 'removeWidget', v: { name: string; id: string; }): void;
(ev: 'updateWidget', v: { id: string; data: Record<string, any>; }): void;
(ev: 'exit'): void;
}>();
context.emit('addWidget', {
name: widgetAdderSelected.value,
id: uuid(),
data: {},
});
const widgetRefs = reactive({});
const configWidget = (id: string) => {
widgetRefs[id].configure();
};
const widgetAdderSelected = ref(null);
const addWidget = () => {
if (widgetAdderSelected.value == null) return;
widgetAdderSelected.value = null;
};
const removeWidget = (widget) => {
context.emit('removeWidget', widget);
};
const updateWidget = (id, data) => {
context.emit('updateWidget', { id, data });
};
const widgets_ = computed({
get: () => props.widgets,
set: (value) => {
context.emit('updateWidgets', value);
},
});
emit('addWidget', {
name: widgetAdderSelected.value,
id: uuid(),
data: {},
});
return {
widgetRefs,
configWidget,
widgetAdderSelected,
widgetDefs,
addWidget,
removeWidget,
updateWidget,
widgets_,
};
widgetAdderSelected.value = null;
};
const removeWidget = (widget) => {
emit('removeWidget', widget);
};
const updateWidget = (id, data) => {
emit('updateWidget', { id, data });
};
const widgets_ = computed({
get: () => props.widgets,
set: (value) => {
emit('updateWidgets', value);
},
});
</script>

View File

@ -150,6 +150,7 @@ export const popups = ref([]) as Ref<{
id: any;
component: any;
props: Record<string, any>;
events: any[];
}[]>;
const zIndexes = {

View File

@ -1,28 +1,29 @@
<template>
<section class="_section">
<div class="_title">{{ $t('_auth.shareAccess', { name: app.name }) }}</div>
<div class="_title">{{ i18n.t('_auth.shareAccess', { name: app.name }) }}</div>
<div class="_content">
<h2>{{ app.name }}</h2>
<p class="id">{{ app.id }}</p>
<p class="description">{{ app.description }}</p>
</div>
<div class="_content">
<h2>{{ $ts._auth.permissionAsk }}</h2>
<h2>{{ i18n.ts._auth.permissionAsk }}</h2>
<ul>
<li v-for="p in app.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
<li v-for="p in app.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
</ul>
</div>
<div class="_footer">
<MkButton inline @click="cancel">{{ $ts.cancel }}</MkButton>
<MkButton inline primary @click="accept">{{ $ts.accept }}</MkButton>
<MkButton inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
<MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton>
</div>
</section>
</template>
<script lang="ts" setup>
import { defineComponent } from 'vue';
import { } from 'vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
const emit = defineEmits<{
(ev: 'denied'): void;

View File

@ -1,26 +1,24 @@
<template>
<div v-if="$i && fetching" class="">
<MkLoading/>
</div>
<div v-else-if="$i">
<div v-if="$i">
<MkLoading v-if="state == 'fetching'"/>
<XForm
v-if="state == 'waiting'"
v-else-if="state == 'waiting'"
ref="form"
class="form"
:session="session"
@denied="state = 'denied'"
@accepted="accepted"
/>
<div v-if="state == 'denied'" class="denied">
<h1>{{ $ts._auth.denied }}</h1>
<div v-else-if="state == 'denied'" class="denied">
<h1>{{ i18n.ts._auth.denied }}</h1>
</div>
<div v-if="state == 'accepted'" class="accepted">
<h1>{{ session.app.isAuthorized ? $t('already-authorized') : $ts.allowed }}</h1>
<p v-if="session.app.callbackUrl">{{ $ts._auth.callback }}<MkEllipsis/></p>
<p v-if="!session.app.callbackUrl">{{ $ts._auth.pleaseGoBack }}</p>
<div v-else-if="state == 'accepted'" class="accepted">
<h1>{{ session.app.isAuthorized ? i18n.t('already-authorized') : i18n.ts.allowed }}</h1>
<p v-if="session.app.callbackUrl">{{ i18n.ts._auth.callback }}<MkEllipsis/></p>
<p v-if="!session.app.callbackUrl">{{ i18n.ts._auth.pleaseGoBack }}</p>
</div>
<div v-if="state == 'fetch-session-error'" class="error">
<p>{{ $ts.somethingHappened }}</p>
<div v-else-if="state == 'fetch-session-error'" class="error">
<p>{{ i18n.ts.somethingHappened }}</p>
</div>
</div>
<div v-else class="signin">
@ -28,64 +26,55 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { onMounted } from 'vue';
import XForm from './auth.form.vue';
import MkSignin from '@/components/signin.vue';
import * as os from '@/os';
import { login } from '@/account';
import { i18n } from '@/i18n';
import { $i } from '@/account';
import { query, appendQuery } from '@/scripts/url';
export default defineComponent({
components: {
XForm,
MkSignin,
},
props: ['token'],
data() {
return {
state: null,
session: null,
fetching: true,
};
},
mounted() {
if (!this.$i) return;
const props = defineProps<{
token: string;
}>();
// Fetch session
os.api('auth/session/show', {
token: this.token,
}).then(session => {
this.session = session;
this.fetching = false;
let state: 'fetching' | 'waiting' | 'denied' | 'accepted' | 'fetch-session-error' = $ref('fetching');
let session = $ref(null);
//
if (this.session.app.isAuthorized) {
os.api('auth/accept', {
token: this.session.token,
}).then(() => {
this.accepted();
});
} else {
this.state = 'waiting';
}
}).catch(error => {
this.state = 'fetch-session-error';
this.fetching = false;
});
},
methods: {
accepted() {
this.state = 'accepted';
if (this.session.app.callbackUrl) {
location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`;
}
}, onLogin(res) {
login(res.i);
},
},
onMounted(() => {
if (!$i) return;
// Fetch session
os.api('auth/session/show', {
token: props.token,
}).then(fetchedSession => {
session = fetchedSession;
//
if (session.app.isAuthorized) {
os.api('auth/accept', {
token: session.token,
}).then(() => {
this.accepted();
});
} else {
state = 'waiting';
}
}).catch(error => {
state = 'fetch-session-error';
});
});
function accepted() {
state = 'accepted';
if (session.app.callbackUrl) {
location.href = appendQuery(session.app.callbackUrl, query({ token: session.token }));
}
}
function onLogin(res) {
login(res.i);
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -1,65 +1,76 @@
<template>
<div class="mk-follow-page">
</div>
<!-- This page does not really have any content, it is mainly processing stuff -->
<MkLoading v-if="state == 'loading'"/>
<MkError v-if="state == 'error'" :final="finalError" @retry="doIt"/>
<div v-if="state == 'done'">{{ i18n.ts.done }}</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import * as Acct from 'misskey-js/built/acct';
import * as os from '@/os';
import { mainRouter } from '@/router';
import { i18n } from '@/i18n';
export default defineComponent({
created() {
const acct = new URL(location.href).searchParams.get('acct');
if (acct == null) return;
let state: 'loading' | 'error' | 'done' = $ref('loading');
let finalError: boolean = $ref(false);
let promise;
async function follow(user) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.t('followConfirm', { name: user.name || user.username }),
});
if (acct.startsWith('https://')) {
promise = os.api('ap/show', {
uri: acct,
});
promise.then(res => {
if (res.type === 'User') {
this.follow(res.object);
} else if (res.type === 'Note') {
mainRouter.push(`/notes/${res.object.id}`);
} else {
os.alert({
type: 'error',
text: 'Not a user',
}).then(() => {
window.close();
});
}
});
} else {
promise = os.api('users/show', Acct.parse(acct));
promise.then(user => {
this.follow(user);
});
}
if (canceled) {
window.close();
return;
}
os.promiseDialog(promise, null, null, this.$ts.fetchingAsApObject);
},
os.apiWithDialog('following/create', {
userId: user.id,
});
}
methods: {
async follow(user) {
const { canceled } = await os.confirm({
type: 'question',
text: this.$t('followConfirm', { name: user.name || user.username }),
});
function doIt() {
// this might be a retry
state = 'loading';
if (canceled) {
window.close();
const acct = new URL(location.href).searchParams.get('acct');
if (acct == null || acct.trim() === '') {
finalError = true;
state = 'error';
return;
}
let promise;
if (acct.startsWith('https://')) {
promise = os.api('ap/show', {
uri: acct,
}).then(res => {
if (res.type === 'User') {
follow(res.object);
} else if (res.type === 'Note') {
mainRouter.push(`/notes/${res.object.id}`);
} else {
os.alert({
type: 'error',
text: 'Not a user',
}).then(() => {
finalError = true;
state = 'error';
});
return;
}
os.apiWithDialog('following/create', {
userId: user.id,
});
},
},
});
state = 'done';
});
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
} else {
os.api('users/show', Acct.parse(acct))
.then(user => follow(user))
.then(() => state = 'done');
}
}
doIt();
</script>

View File

@ -1,7 +1,7 @@
<template>
<div class="civpbkhh">
<div ref="scroll" class="scrollbox" v-bind:class="{ scroll: isScrolling }">
<div v-for="note in notes" class="note">
<div ref="scroll" class="scrollbox" :class="{ scroll: isScrolling }">
<div v-for="note in notes" :key="note.id" class="note">
<div class="content _panel">
<div class="body">
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA>
@ -12,7 +12,7 @@
<XMediaList :media-list="note.files"/>
</div>
<div v-if="note.poll">
<XPoll :note="note" :readOnly="true"/>
<XPoll :note="note" :read-only="true"/>
</div>
</div>
<XReactionsViewer ref="reactionsViewer" :note="note"/>
@ -21,37 +21,26 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { onUpdated, ref, Ref } from 'vue';
import { Note } from 'misskey-js/built/entities';
import XReactionsViewer from '@/components/reactions-viewer.vue';
import XMediaList from '@/components/media-list.vue';
import XPoll from '@/components/poll.vue';
import * as os from '@/os';
import { $i } from '@/account';
export default defineComponent({
components: {
XReactionsViewer,
XMediaList,
XPoll
},
const notes: Ref<Note[]> = ref([]);
const isScrolling = ref(false);
const scroll: Ref<HTMLElement | null> = ref(null);
data() {
return {
notes: [],
isScrolling: false,
};
},
os.api('notes/featured').then(newNotes => {
notes.value = newNotes;
});
created() {
os.api('notes/featured').then(notes => {
this.notes = notes;
});
},
updated() {
if (this.$refs.scroll.clientHeight > window.innerHeight) {
this.isScrolling = true;
}
onUpdated(() => {
if (scroll.value && scroll.value.clientHeight > window.innerHeight) {
isScrolling.value = true;
}
});
</script>

View File

@ -1,5 +1,6 @@
<template>
<component :is="popup.component"
<component
:is="popup.component"
v-for="popup in popups"
:key="popup.id"
v-bind="popup.props"
@ -15,56 +16,44 @@
<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import { popup, popups, pendingApiRequestsCount } from '@/os';
<script lang="ts" setup>
import { defineAsyncComponent, Ref, ref } from 'vue';
import { swInject } from './sw-inject';
import { popup as showPopup, popups, pendingApiRequestsCount } from '@/os';
import { uploads } from '@/scripts/upload';
import * as sound from '@/scripts/sound';
import { $i } from '@/account';
import { swInject } from './sw-inject';
import { stream } from '@/stream';
export default defineComponent({
components: {
XStreamIndicator: defineAsyncComponent(() => import('./stream-indicator.vue')),
XUpload: defineAsyncComponent(() => import('./upload.vue')),
},
const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
const XUpload = defineAsyncComponent(() => import('./upload.vue'));
const dev: Ref<boolean> = ref(_DEV_);
setup() {
const onNotification = notification => {
if ($i.mutingNotificationTypes.includes(notification.type)) return;
const onNotification = (notification: { type: string; id: any; }): void => {
if ($i?.mutingNotificationTypes.includes(notification.type)) return;
if (document.visibilityState === 'visible') {
stream.send('readNotification', {
id: notification.id
});
if (document.visibilityState === 'visible') {
stream.send('readNotification', {
id: notification.id,
});
popup(defineAsyncComponent(() => import('@/components/notification-toast.vue')), {
notification
}, {}, 'closed');
}
showPopup(defineAsyncComponent(() => import('@/components/notification-toast.vue')), {
notification
}, {}, 'closed');
}
sound.play('notification');
};
sound.play('notification');
};
if ($i) {
const connection = stream.useChannel('main', null, 'UI');
connection.on('notification', onNotification);
if ($i) {
const connection = stream.useChannel('main', null, 'UI');
connection.on('notification', onNotification);
//#region Listen message from SW
if ('serviceWorker' in navigator) {
swInject();
}
}
return {
uploads,
popups,
pendingApiRequestsCount,
dev: _DEV_,
};
},
});
//#region Listen message from SW
if ('serviceWorker' in navigator) {
swInject();
}
}
</script>
<style lang="scss">

View File

@ -3,17 +3,8 @@
<XCommon/>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import DesignA from './visitor/a.vue';
<script lang="ts" setup>
import { } from 'vue';
import DesignB from './visitor/b.vue';
import XCommon from './_common_/common.vue';
export default defineComponent({
components: {
XCommon,
DesignA,
DesignB,
},
});
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="sqxihjet">
<div ref="header" class="sqxihjet">
<div v-if="narrow === false" class="wide">
<div class="content">
<MkA to="/" class="link" active-class="active"><i class="fas fa-home icon"></i>{{ $ts.home }}</MkA>
@ -13,7 +13,9 @@
<span v-if="info.title" class="text">{{ info.title }}</span>
<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/>
</div>
<button v-if="info.action" class="_button action" @click.stop="info.action.handler"><!-- TODO --></button>
<button v-if="info.action" class="_button action" @click.stop="info.action.handler">
<!-- TODO -->
</button>
</div>
<div class="right">
<button class="_button search" @click="search()"><i class="fas fa-search icon"></i><span>{{ $ts.search }}</span></button>
@ -22,8 +24,8 @@
</div>
</div>
</div>
<div v-else-if="narrow === true" class="narrow">
<button class="menu _button" @click="$parent.showMenu = true">
<div v-else-if="narrow" class="narrow">
<button class="menu _button" @click="showMenu = true">
<i class="fas fa-bars icon"></i>
</button>
<div v-if="info" class="title">
@ -39,47 +41,38 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { onMounted, ref, Ref } from 'vue';
import XSigninDialog from '@/components/signin-dialog.vue';
import XSignupDialog from '@/components/signup-dialog.vue';
import * as os from '@/os';
import { search } from '@/scripts/search';
export default defineComponent({
props: {
info: {
required: true
},
},
defineProps<{
info: any;
}>();
data() {
return {
narrow: null,
showMenu: false,
};
},
const narrow = ref(false);
const showMenu = ref(false);
const header: Ref<HTMLElement | null> = ref(null);
mounted() {
this.narrow = this.$el.clientWidth < 1300;
},
methods: {
signin() {
os.popup(XSigninDialog, {
autoSet: true
}, {}, 'closed');
},
signup() {
os.popup(XSignupDialog, {
autoSet: true
}, {}, 'closed');
},
search
onMounted(() => {
if (header.value) {
narrow.value = header.value.clientWidth < 1300;
}
});
function signin(): void {
os.popup(XSigninDialog, {
autoSet: true,
}, {}, 'closed');
}
function signup(): void {
os.popup(XSignupDialog, {
autoSet: true,
}, {}, 'closed');
}
</script>
<style lang="scss" scoped>
@ -94,14 +87,14 @@ export default defineComponent({
backdrop-filter: var(--blur, blur(32px));
background-color: var(--X16);
> .wide {
> .content {
>.wide {
>.content {
max-width: 1400px;
margin: 0 auto;
display: flex;
align-items: center;
> .link {
>.link {
$line: 3px;
display: inline-block;
padding: 0 16px;
@ -109,7 +102,7 @@ export default defineComponent({
border-top: solid $line transparent;
border-bottom: solid $line transparent;
> .icon {
>.icon {
margin-right: 0.5em;
}
@ -118,8 +111,8 @@ export default defineComponent({
}
}
> .page {
> .title {
>.page {
>.title {
display: inline-block;
vertical-align: bottom;
white-space: nowrap;
@ -127,11 +120,11 @@ export default defineComponent({
text-overflow: ellipsis;
position: relative;
> .icon + .text {
>.icon+.text {
margin-left: 8px;
}
> .avatar {
>.avatar {
$size: 32px;
display: inline-block;
width: $size;
@ -151,76 +144,72 @@ export default defineComponent({
box-shadow: 0 -2px 0 0 var(--accent) inset;
color: var(--fgHighlighted);
}
}
> .action {
padding: 0 0 0 16px;
}
}
el>.right {
margin-left: auto;
> .right {
margin-left: auto;
>.search {
background: var(--bg);
border-radius: 999px;
width: 230px;
line-height: $height - 20px;
margin-right: 16px;
text-align: left;
> .search {
background: var(--bg);
border-radius: 999px;
width: 230px;
line-height: $height - 20px;
margin-right: 16px;
text-align: left;
>* {
opacity: 0.7;
}
> * {
opacity: 0.7;
}
>.icon {
padding: 0 16px;
}
}
> .icon {
padding: 0 16px;
>.signup {
border-radius: 999px;
padding: 0 24px;
line-height: $height - 20px;
}
>.login {
padding: 0 16px;
}
}
}
> .signup {
border-radius: 999px;
padding: 0 24px;
line-height: $height - 20px;
}
> .login {
padding: 0 16px;
}
}
}
}
> .narrow {
display: flex;
> .menu,
> .action {
width: $height;
height: $height;
font-size: 20px;
}
> .title {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
position: relative;
text-align: center;
> .icon + .text {
margin-left: 8px;
}
> .avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: middle;
margin-right: 8px;
pointer-events: none;
>.narrow {
display: flex;
>.menu,
>.action {
width: $height;
height: $height;
font-size: 20px;
}
>.title {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
position: relative;
text-align: center;
>.icon+.text {
margin-left: 8px;
}
>.avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: middle;
margin-right: 8px;
pointer-events: none;
}
}
}
}
}