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 { return {
id: app.id, id: app.id,
name: app.name, name: app.name,
description: app.description,
callbackUrl: app.callbackUrl, callbackUrl: app.callbackUrl,
permission: app.permission, permission: app.permission,
...(opts.includeSecret ? { secret: app.secret } : {}), ...(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 { DriveFile } from '@/models/entities/drive-file.js';
import { Note } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { Channel } from '@/models/entities/channel.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 { noteVisibilities } from '../../../../types.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
import define from '../../define.js'; import define from '../../define.js';
import { HOUR } from '@/const.js';
import { getNote } from '../../common/getters.js'; import { getNote } from '../../common/getters.js';
export const meta = { export const meta = {
@ -78,13 +77,24 @@ export const meta = {
code: 'YOU_HAVE_BEEN_BLOCKED', code: 'YOU_HAVE_BEEN_BLOCKED',
id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3', 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; } as const;
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: { 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: { visibleUserIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id', type: 'string', format: 'misskey:id',
} }, } },
@ -195,6 +205,11 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.cannotReRenote); 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 // Check blocking
if (renote.userId !== user.id) { if (renote.userId !== user.id) {
const block = await Blockings.findOneBy({ const block = await Blockings.findOneBy({
@ -219,6 +234,11 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.cannotReplyToPureRenote); 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 // Check blocking
if (reply.userId !== user.id) { if (reply.userId !== user.id) {
const block = await Blockings.findOneBy({ 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'; data.visibility = 'followers';
} }
// 返信対象がpublicではないならhomeにする
if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') {
data.visibility = 'home';
}
// ローカルのみをRenoteしたらローカルのみにする // ローカルのみをRenoteしたらローカルのみにする
if (data.renote && data.renote.localOnly && data.channel == null) { if (data.renote && data.renote.localOnly && data.channel == null) {
data.localOnly = true; 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; 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 noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;

View file

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

View file

@ -3,7 +3,7 @@
<div class="mjndxjcg"> <div class="mjndxjcg">
<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> <img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
<p><i class="fas fa-exclamation-triangle"></i> {{ i18n.ts.somethingHappened }}</p> <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> </div>
</transition> </transition>
</template> </template>
@ -11,6 +11,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import { i18n } from '@/i18n'; 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

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

View file

@ -7,7 +7,7 @@ import MkEmoji from '@/components/global/emoji.vue';
import { concat } from '@/scripts/array'; import { concat } from '@/scripts/array';
import MkFormula from '@/components/formula.vue'; import MkFormula from '@/components/formula.vue';
import MkCode from '@/components/code.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 MkSparkle from '@/components/sparkle.vue';
import MkA from '@/components/global/a.vue'; import MkA from '@/components/global/a.vue';
import { host } from '@/config'; import { host } from '@/config';
@ -306,7 +306,7 @@ export default defineComponent({
} }
case 'search': { case 'search': {
return [h(MkGoogle, { return [h(MkSearch, {
key: Math.random(), key: Math.random(),
q: token.props.query, 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="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) && isEmpty(value)" class="array empty">[]</div>
<div v-else-if="isArray(value)" class="array"> <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/> {{ i }}: <XValue :value="value[i - 1]" collapsed/>
</div> </div>
</div> </div>
<div v-else-if="isObject(value) && isEmpty(value)" class="object empty">{}</div> <div v-else-if="isObject(value) && isEmpty(value)" class="object empty">{}</div>
<div v-else-if="isObject(value)" class="object"> <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> <button class="toggle _button" :class="{ visible: collapsable(value[k]) }" @click="collapsed[k] = !collapsed[k]">{{ collapsed[k] ? '+' : '-' }}</button>
<div class="k">{{ k }}:</div> <div class="k">{{ k }}:</div>
<div v-if="collapsed[k]" class="v"> <div v-if="collapsed[k]" class="v">
@ -28,54 +28,38 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent, reactive, ref } from 'vue'; import { reactive, defineProps } from 'vue';
import number from '@/filters/number'; import number from '@/filters/number';
export default defineComponent({ const props = defineProps<{
name: 'XValue', value: any;
}>();
props: { const collapsed = reactive({});
value: {
required: true,
},
},
setup(props) { if (isObject(props.value)) {
const collapsed = reactive({}); for (const key in props.value) {
collapsed[key] = collapsable(props.value[key]);
}
}
if (isObject(props.value)) { function isObject(v): boolean {
for (const key in props.value) { return typeof v === 'object' && !Array.isArray(v) && v !== null;
collapsed[key] = collapsable(props.value[key]); }
}
}
function isObject(v): boolean { function isArray(v): boolean {
return typeof v === 'object' && !Array.isArray(v) && v !== null; return Array.isArray(v);
} }
function isArray(v): boolean { function isEmpty(v): boolean {
return Array.isArray(v); return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
} }
function isEmpty(v): boolean { function collapsable(v): boolean {
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0); 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -27,90 +27,70 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { ref } from 'vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
import MkSwitch from '@/components/form/switch.vue'; import MkSwitch from '@/components/form/switch.vue';
import MkTextarea from '@/components/form/textarea.vue';
import MkRadio from '@/components/form/radio.vue'; import MkRadio from '@/components/form/radio.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as config from '@/config'; import * as config from '@/config';
import { $i } from '@/account';
export default defineComponent({ const text = ref('');
components: { const flag = ref(true);
MkButton, const radio = ref('misskey');
MkInput, 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.`);
MkSwitch,
MkTextarea,
MkRadio,
},
data() { function openDialog(): void {
return { os.alert({
text: '', type: 'warning',
flag: true, title: 'Oh my Aichan',
radio: 'misskey', text: 'Lorem ipsum dolor sit amet, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
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.` });
}; }
},
methods: { function openForm(): void {
async openDialog() { os.form('Example form', {
os.alert({ foo: {
type: 'warning', type: 'boolean',
title: 'Oh my Aichan', default: true,
text: 'Lorem ipsum dolor sit amet, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', label: 'This is a boolean property',
});
}, },
bar: {
async openForm() { type: 'number',
os.form('Example form', { default: 300,
foo: { label: 'This is a number property',
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'
},
});
}, },
baz: {
async openDrive() { type: 'string',
os.selectDriveFile(); default: 'Misskey makes you happy.',
label: 'This is a string property',
}, },
});
}
async selectUser() { function openDrive(): void {
os.selectUser(); os.selectDriveFile(true);
}, }
async openMenu(ev) { function openMenu(ev): void {
os.popupMenu([{ os.popupMenu([{
type: 'label', type: 'label',
text: 'Fruits' text: 'Fruits'
}, { }, {
text: 'Create some apples', text: 'Create some apples',
action: () => {}, action: (): void => {},
}, { }, {
text: 'Read some oranges', text: 'Read some oranges',
action: () => {}, action: (): void => {},
}, { }, {
text: 'Update some melons', text: 'Update some melons',
action: () => {}, action: (): void => {},
}, null, { }, null, {
text: 'Delete some bananas', text: 'Delete some bananas',
danger: true, danger: true,
action: () => {}, action: (): void => {},
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);
}, }
}
});
</script> </script>

View file

@ -31,7 +31,7 @@ type ModalTypes = 'popup' | 'dialog' | 'dialog:top' | 'drawer';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
manualShowing?: boolean | null; manualShowing?: boolean | null;
anchor?: { x: string; y: string; }; anchor?: { x: string; y: string; };
src?: HTMLElement; src?: HTMLElement | null;
preferType?: ModalTypes | 'auto'; preferType?: ModalTypes | 'auto';
zPriority?: 'low' | 'middle' | 'high'; zPriority?: 'low' | 'middle' | 'high';
noOverlap?: boolean; noOverlap?: boolean;
@ -102,7 +102,7 @@ const MARGIN = 16;
const align = () => { const align = () => {
if (props.src == null) return; if (props.src == null) return;
if (type === 'drawer') return; if (type === 'drawer') return;
if (type == 'dialog') return; if (type === 'dialog') return;
if (content == null) 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> <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option>
</MkSelect> </MkSelect>
<MkButton inline primary class="mk-widget-add" @click="addWidget"><i class="fas fa-plus"></i> {{ i18n.ts.add }}</MkButton> <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> </header>
<XDraggable <XDraggable
v-model="widgets_" v-model="widgets_"
@ -30,73 +30,57 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, defineAsyncComponent, reactive, ref, computed } from 'vue'; import { defineComponent, defineAsyncComponent, reactive, ref, computed } from 'vue';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/form/select.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import { widgets as widgetDefs } from '@/widgets'; import { widgets as widgetDefs } from '@/widgets';
import { i18n } from '@/i18n';
export default defineComponent({ const XDraggable = defineAsyncComponent(() => import('vuedraggable'));
components: {
XDraggable: defineAsyncComponent(() => import('vuedraggable')),
MkSelect,
MkButton,
},
props: { type Widgets = any[];
widgets: {
type: Array,
required: true,
},
edit: {
type: Boolean,
required: true,
},
},
emits: ['updateWidgets', 'addWidget', 'removeWidget', 'updateWidget', 'exit'], const props = defineProps<{
widgets: Widgets;
edit: boolean;
}>();
setup(props, context) { const emit = defineEmits<{
const widgetRefs = reactive({}); (ev: 'updateWidgets', v: Widgets): void;
const configWidget = (id: string) => { (ev: 'addWidget', v: { name: string; id: string; data: Record<string, any>; }): void;
widgetRefs[id].configure(); (ev: 'removeWidget', v: { name: string; id: string; }): void;
}; (ev: 'updateWidget', v: { id: string; data: Record<string, any>; }): void;
const widgetAdderSelected = ref(null); (ev: 'exit'): void;
const addWidget = () => { }>();
if (widgetAdderSelected.value == null) return;
context.emit('addWidget', { const widgetRefs = reactive({});
name: widgetAdderSelected.value, const configWidget = (id: string) => {
id: uuid(), widgetRefs[id].configure();
data: {}, };
}); const widgetAdderSelected = ref(null);
const addWidget = () => {
if (widgetAdderSelected.value == null) return;
widgetAdderSelected.value = null; emit('addWidget', {
}; name: widgetAdderSelected.value,
const removeWidget = (widget) => { id: uuid(),
context.emit('removeWidget', widget); data: {},
}; });
const updateWidget = (id, data) => {
context.emit('updateWidget', { id, data });
};
const widgets_ = computed({
get: () => props.widgets,
set: (value) => {
context.emit('updateWidgets', value);
},
});
return { widgetAdderSelected.value = null;
widgetRefs, };
configWidget, const removeWidget = (widget) => {
widgetAdderSelected, emit('removeWidget', widget);
widgetDefs, };
addWidget, const updateWidget = (id, data) => {
removeWidget, emit('updateWidget', { id, data });
updateWidget, };
widgets_, const widgets_ = computed({
}; get: () => props.widgets,
set: (value) => {
emit('updateWidgets', value);
}, },
}); });
</script> </script>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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