Merge branch 'main' into feat/secure-fetch
This commit is contained in:
commit
66df12df0a
20 changed files with 495 additions and 624 deletions
|
@ -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 } : {}),
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
})];
|
})];
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue