Compare commits

...

12 Commits

Author SHA1 Message Date
syuilo
adbe0fbcd1 10.49.1 2018-11-13 16:33:21 +09:00
syuilo
7896242f57 [Client] Fix #3213 2018-11-13 16:19:46 +09:00
syuilo
4a6722b9e9 Merge branch 'master' into develop 2018-11-13 15:01:52 +09:00
syuilo
7c9fb5228b Improve usability 2018-11-13 15:01:05 +09:00
MeiMei
81805b01cc sort user timeline by date (#3210) 2018-11-13 01:17:59 +09:00
syuilo
50824a7245 10.49.0 2018-11-13 01:12:27 +09:00
syuilo
6f2953f3a7 [Client] Clear cached locale data when shouldFlush is true 2018-11-13 01:11:36 +09:00
syuilo
dd3f007582 [Client] Improve post-form widget 2018-11-13 01:04:15 +09:00
syuilo
a4b2b093fc 🎨 2018-11-13 00:21:49 +09:00
syuilo
0fbf56219f [Client] Emoji picker
Closes #3130
2018-11-13 00:12:55 +09:00
syuilo
0acacf7a8e 10.48.1 2018-11-12 06:12:45 +09:00
syuilo
c84500d914 Clean up 2018-11-12 06:12:22 +09:00
32 changed files with 895 additions and 497 deletions

View File

@@ -379,6 +379,17 @@ common/views/components/poll-editor.vue:
common/views/components/reaction-picker.vue:
choose-reaction: "リアクションを選択"
common/views/components/emoji-picker.vue:
custom-emoji: "カスタム絵文字"
people: "人"
animals-and-nature: "動物&自然"
food-and-drink: "食べ物&飲み物"
activity: "アクティビティ"
travel-and-places: "場所"
objects: "物"
symbols: "記号"
flags: "旗"
common/views/components/signin.vue:
username: "ユーザー名"
password: "パスワード"
@@ -657,13 +668,6 @@ desktop/views/components/media-video.vue:
sensitive: "閲覧注意"
click-to-show: "クリックして表示"
desktop/views/components/follow-button.vue:
following: "フォロー中"
follow: "フォロー"
request-pending: "フォロー許可待ち"
follow-processing: "フォロー処理中"
follow-request: "フォロー申請"
desktop/views/components/followers-window.vue:
followers: "{} のフォロワー"
@@ -1325,7 +1329,7 @@ mobile/views/components/media-video.vue:
sensitive: "閲覧注意"
click-to-show: "クリックして表示"
mobile/views/components/follow-button.vue:
common/views/components/follow-button.vue:
following: "フォロー中"
follow: "フォロー"
request-pending: "フォロー許可待ち"

View File

@@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "10.48.0",
"clientVersion": "2.0.11733",
"version": "10.49.1",
"clientVersion": "2.0.11745",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,

View File

@@ -66,19 +66,6 @@ export default abstract class Chart<T> {
} else {
this.collection.createIndex({ span: -1, date: -1 }, { unique: true });
}
//#region 後方互換性のため
this.collection.find({ span: 'day' }, { fields: { _id: true, date: true } }).then(logs => {
logs.forEach(log => {
this.collection.update({ _id: log._id }, { $set: { date: utc(log.date).hour(0).toDate() } });
});
});
this.collection.find({ span: 'hour' }, { fields: { _id: true, date: true } }).then(logs => {
logs.forEach(log => {
this.collection.update({ _id: log._id }, { $set: { date: utc(log.date).toDate() } });
});
});
//#endregion
}
@autobind

View File

@@ -146,6 +146,8 @@
function refresh() {
localStorage.setItem('shouldFlush', 'false');
localStorage.removeItem('locale');
// Random
localStorage.setItem('salt', Math.random().toString().substr(2, 8));

View File

@@ -0,0 +1,200 @@
<template>
<div class="prlncendiewqqkrevzeruhndoakghvtx">
<header>
<button v-for="category in categories"
:title="category.text"
@click="go(category.ref)"
:class="{ active: category.isActive }"
>
<fa :icon="category.icon" fixed-width/>
</button>
</header>
<div class="emojis" ref="emojis" @scroll.passive="onScroll">
<section v-for="category in categories" :ref="category.ref">
<header><fa :icon="category.icon" fixed-width/> {{ category.text }}</header>
<div v-if="category.name">
<button v-for="emoji in Object.entries(lib).filter(([k, v]) => v.category === category.name)"
:title="emoji[0]"
@click="chosen(emoji[1].char)"
>
<mk-emoji :emoji="emoji[1].char"/>
</button>
</div>
<div v-else>
<button v-for="emoji in customEmojis"
:title="emoji.name"
@click="chosen(`:${emoji.name}:`)"
>
<img :src="emoji.url" :alt="emoji.name"/>
</button>
</div>
</section>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import { lib } from 'emojilib';
export default Vue.extend({
i18n: i18n('common/views/components/emoji-picker.vue'),
data() {
return {
lib,
customEmojis: [],
categories: [{
ref: 'customEmojiSection',
text: this.$t('custom-emoji'),
icon: ['fas', 'asterisk'],
isActive: true
}, {
name: 'people',
ref: 'peopleSection',
text: this.$t('people'),
icon: ['far', 'laugh'],
isActive: false
}, {
name: 'animals_and_nature',
ref: 'animalsAndNatureSection',
text: this.$t('animals-and-nature'),
icon: ['fas', 'leaf'],
isActive: false
}, {
name: 'food_and_drink',
ref: 'foodAndDrinkSection',
text: this.$t('food-and-drink'),
icon: ['fas', 'utensils'],
isActive: false
}, {
name: 'activity',
ref: 'activitySection',
text: this.$t('activity'),
icon: ['fas', 'futbol'],
isActive: false
}, {
name: 'travel_and_places',
ref: 'travelAndPlacesSection',
text: this.$t('travel-and-places'),
icon: ['fas', 'city'],
isActive: false
}, {
name: 'objects',
ref: 'objectsSection',
text: this.$t('objects'),
icon: ['fas', 'poo-storm'],
isActive: false
}, {
name: 'symbols',
ref: 'symbolsSection',
text: this.$t('symbols'),
icon: ['far', 'heart'],
isActive: false
}, {
name: 'flags',
ref: 'flagsSection',
text: this.$t('flags'),
icon: ['far', 'flag'],
isActive: false
}]
}
},
created() {
this.customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
},
methods: {
go(ref) {
this.$refs.emojis.scrollTop = this.$refs[ref][0].offsetTop;
},
onScroll(e) {
const section = this.categories.forEach(x => {
const top = e.target.scrollTop;
const el = this.$refs[x.ref][0];
x.isActive = el.offsetTop <= top && el.offsetTop + el.offsetHeight > top;
});
},
chosen(emoji) {
this.$emit('chosen', emoji);
}
}
});
</script>
<style lang="stylus" scoped>
.prlncendiewqqkrevzeruhndoakghvtx
width 350px
background var(--face)
> header
display flex
> button
flex 1
padding 10px 0
font-size 16px
color var(--text)
transition color 0.2s ease
&:hover
color var(--textHighlighted)
transition color 0s
&.active
color var(--primary)
transition color 0s
> .emojis
height 300px
overflow-y auto
overflow-x hidden
> section
> header
position sticky
top 0
left 0
z-index 1
padding 8px
background var(--faceHeader)
color var(--text)
font-size 12px
> div
display grid
grid-template-columns 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr
gap 4px
padding 8px
> button
padding 0
width 100%
&:before
content ''
display block
width 1px
height 0
padding-bottom 100%
&:hover
> *
transform scale(1.2)
transition transform 0s
> *
position absolute
top 0
left 0
width 100%
height 100%
font-size 28px
transition transform 0.2s ease
pointer-events none
</style>

View File

@@ -22,7 +22,7 @@ export default Vue.extend({
},
customEmojis: {
required: false,
default: []
default: () => []
}
},

View File

@@ -0,0 +1,184 @@
<template>
<button class="wfliddvnhxvyusikowhxozkyxyenqxqr"
:class="{ wait, block, mini, active: isFollowing || hasPendingFollowRequestFromYou }"
@click="onClick"
:disabled="wait"
>
<template v-if="!wait">
<fa :icon="iconAndText[0]"/> <template v-if="!mini">{{ iconAndText[1] }}</template>
</template>
<template v-else><fa icon="spinner" pulse fixed-width/></template>
</button>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
export default Vue.extend({
i18n: i18n('common/views/components/follow-button.vue'),
props: {
user: {
type: Object,
required: true
},
block: {
type: Boolean,
required: false,
default: false
},
mini: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
isFollowing: this.user.isFollowing,
hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou,
wait: false,
connection: null
};
},
computed: {
iconAndText(): any[] {
return (
(this.hasPendingFollowRequestFromYou && this.user.isLocked) ? ['hourglass-half', this.$t('request-pending')] :
(this.hasPendingFollowRequestFromYou && !this.user.isLocked) ? ['hourglass-start', this.$t('follow-processing')] :
(this.isFollowing) ? ['minus', this.$t('following')] :
(!this.isFollowing && this.user.isLocked) ? ['plus', this.$t('follow-request')] :
(!this.isFollowing && !this.user.isLocked) ? ['plus', this.$t('follow')] :
[]
);
}
},
mounted() {
this.connection = this.$root.stream.useSharedConnection('main');
this.connection.on('follow', this.onFollowChange);
this.connection.on('unfollow', this.onFollowChange);
},
beforeDestroy() {
this.connection.dispose();
},
methods: {
onFollowChange(user) {
if (user.id == this.user.id) {
this.isFollowing = user.isFollowing;
this.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
}
},
async onClick() {
this.wait = true;
try {
if (this.isFollowing) {
await this.$root.api('following/delete', {
userId: this.user.id
});
} else {
if (this.hasPendingFollowRequestFromYou) {
await this.$root.api('following/requests/cancel', {
userId: this.user.id
});
} else if (this.user.isLocked) {
await this.$root.api('following/create', {
userId: this.user.id
});
this.hasPendingFollowRequestFromYou = true;
} else {
await this.$root.api('following/create', {
userId: this.user.id
});
this.hasPendingFollowRequestFromYou = true;
}
}
} catch (e) {
console.error(e);
} finally {
this.wait = false;
}
}
}
});
</script>
<style lang="stylus" scoped>
.wfliddvnhxvyusikowhxozkyxyenqxqr
display block
user-select none
cursor pointer
padding 0 16px
margin 0
min-width 100px
line-height 36px
font-size 14px
font-weight bold
color var(--primary)
background transparent
outline none
border solid 1px var(--primary)
border-radius 36px
&.mini
padding 0
min-width 0
width 32px
height 32px
font-size 16px
border-radius 4px
line-height 32px
&:focus
&:after
border-radius 8px
&.block
width 100%
&:focus
&:after
content ""
pointer-events none
position absolute
top -5px
right -5px
bottom -5px
left -5px
border 2px solid var(--primaryAlpha03)
border-radius 36px
&:hover
background var(--primaryAlpha01)
&:active
background var(--primaryAlpha02)
&.active
color var(--primaryForeground)
background var(--primary)
&:hover
background var(--primaryLighten10)
border-color var(--primaryLighten10)
&:active
background var(--primaryDarken10)
border-color var(--primaryDarken10)
&.wait
cursor wait !important
opacity 0.7
*
pointer-events none
</style>

View File

@@ -1,5 +1,6 @@
import Vue from 'vue';
import followButton from './follow-button.vue';
import muteAndBlock from './mute-and-block.vue';
import error from './error.vue';
import apiSettings from './api-settings.vue';
@@ -51,6 +52,7 @@ import uiInfo from './ui/info.vue';
import formButton from './ui/form/button.vue';
import formRadio from './ui/form/radio.vue';
Vue.component('mk-follow-button', followButton);
Vue.component('mk-mute-and-block', muteAndBlock);
Vue.component('mk-error', error);
Vue.component('mk-api-settings', apiSettings);

View File

@@ -5,7 +5,13 @@
<p :class="$style.fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
<div :class="$style.stream" v-if="!fetching && images.length > 0">
<div v-for="image in images" :class="$style.img" :style="`background-image: url(${image.thumbnailUrl || image.url})`"></div>
<div v-for="image in images"
:class="$style.img"
:style="`background-image: url(${image.thumbnailUrl || image.url})`"
draggable="true"
@dragstart="onDragstart(image, $event)"
@dragend="onDragend"
></div>
</div>
<p :class="$style.empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p>
</mk-widget-container>
@@ -31,6 +37,7 @@ export default define({
connection: null
};
},
mounted() {
this.connection = this.$root.stream.useSharedConnection('main');
@@ -44,9 +51,11 @@ export default define({
this.fetching = false;
});
},
beforeDestroy() {
this.connection.dispose();
},
methods: {
onDriveFileCreated(file) {
if (/^image\/.+$/.test(file.type)) {
@@ -54,6 +63,7 @@ export default define({
if (this.images.length > 9) this.images.pop();
}
},
func() {
if (this.props.design == 2) {
this.props.design = 0;
@@ -62,7 +72,16 @@ export default define({
}
this.save();
}
},
onDragstart(file, e) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('mk_drive_file', JSON.stringify(file));
},
onDragend(e) {
this.browser.isDragSource = false;
},
}
});
</script>

View File

@@ -0,0 +1,84 @@
<template>
<div class="gcafiosrssbtbnbzqupfmglvzgiaipyv">
<x-picker @chosen="chosen"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import contains from '../../../common/scripts/contains';
export default Vue.extend({
components: {
XPicker: () => import('../../../common/views/components/emoji-picker.vue').then(m => m.default)
},
props: {
x: {
type: Number,
required: true
},
y: {
type: Number,
required: true
}
},
mounted() {
this.$nextTick(() => {
const width = this.$el.offsetWidth;
const height = this.$el.offsetHeight;
let x = this.x;
let y = this.y;
if (x + width - window.pageXOffset > window.innerWidth) {
x = window.innerWidth - width + window.pageXOffset;
}
if (y + height - window.pageYOffset > window.innerHeight) {
y = window.innerHeight - height + window.pageYOffset;
}
this.$el.style.left = x + 'px';
this.$el.style.top = y + 'px';
Array.from(document.querySelectorAll('body *')).forEach(el => {
el.addEventListener('mousedown', this.onMousedown);
});
});
},
methods: {
onMousedown(e) {
e.preventDefault();
if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
return false;
},
chosen(emoji) {
this.$emit('chosen', emoji);
this.close();
},
close() {
Array.from(document.querySelectorAll('body *')).forEach(el => {
el.removeEventListener('mousedown', this.onMousedown);
});
this.$emit('closed');
this.destroyDom();
}
}
});
</script>
<style lang="stylus" scoped>
.gcafiosrssbtbnbzqupfmglvzgiaipyv
position fixed
top 0
left 0
z-index 3000
box-shadow 0 2px 12px 0 rgba(0, 0, 0, 0.3)
</style>

View File

@@ -1,157 +0,0 @@
<template>
<button class="mk-follow-button"
:class="{ wait, active: u.isFollowing || u.hasPendingFollowRequestFromYou, big: size == 'big' }"
@click="onClick"
:disabled="wait"
>
<template v-if="!wait">
<template v-if="u.hasPendingFollowRequestFromYou && u.isLocked"><fa icon="hourglass-half"/><template v-if="size == 'big'"> {{ $t('request-pending') }}</template></template>
<template v-else-if="u.hasPendingFollowRequestFromYou && !u.isLocked"><fa icon="hourglass-start"/><template v-if="size == 'big'"> {{ $t('follow-processing') }}</template></template>
<template v-else-if="u.isFollowing"><fa icon="minus"/><template v-if="size == 'big'"> {{ $t('following') }}</template></template>
<template v-else-if="!u.isFollowing && u.isLocked"><fa icon="plus"/><template v-if="size == 'big'"> {{ $t('follow-request') }}</template></template>
<template v-else-if="!u.isFollowing && !u.isLocked"><fa icon="plus"/><template v-if="size == 'big'"> {{ $t('follow') }}</template></template>
</template>
<template v-else><fa icon="spinner .pulse" fixed-width/></template>
</button>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
export default Vue.extend({
i18n: i18n('desktop/views/components/follow-button.vue'),
props: {
user: {
type: Object,
required: true
},
size: {
type: String,
default: 'compact'
}
},
data() {
return {
u: this.user,
wait: false,
connection: null
};
},
mounted() {
this.connection = this.$root.stream.useSharedConnection('main');
this.connection.on('follow', this.onFollowChange);
this.connection.on('unfollow', this.onFollowChange);
},
beforeDestroy() {
this.connection.dispose();
},
methods: {
onFollowChange(user) {
if (user.id == this.u.id) {
this.u.isFollowing = user.isFollowing;
this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
this.$forceUpdate();
}
},
async onClick() {
this.wait = true;
try {
if (this.u.isFollowing) {
this.u = await this.$root.api('following/delete', {
userId: this.u.id
});
} else {
if (this.u.hasPendingFollowRequestFromYou) {
this.u = await this.$root.api('following/requests/cancel', {
userId: this.u.id
});
} else if (this.u.isLocked) {
this.u = await this.$root.api('following/create', {
userId: this.u.id
});
} else {
this.u = await this.$root.api('following/create', {
userId: this.user.id
});
}
}
} catch (e) {
console.error(e);
} finally {
this.wait = false;
}
}
}
});
</script>
<style lang="stylus" scoped>
.mk-follow-button
display block
cursor pointer
padding 0
margin 0
width 32px
height 32px
font-size 1em
outline none
border-radius 4px
*
pointer-events none
&:focus
&:after
content ""
pointer-events none
position absolute
top -5px
right -5px
bottom -5px
left -5px
border 2px solid var(--primaryAlpha03)
border-radius 8px
&:not(.active)
color var(--primary)
border solid 1px var(--primary)
&:hover
background var(--primaryAlpha03)
&:active
background var(--primaryAlpha05)
&.active
color var(--primaryForeground)
background var(--primary)
border solid 1px var(--primary)
&:not(:disabled)
font-weight bold
&:hover:not(:disabled)
background var(--primaryLighten5)
border-color var(--primaryLighten5)
&:active:not(:disabled)
background var(--primaryDarken5)
border-color var(--primaryDarken5)
&.wait
cursor wait !important
opacity 0.7
&.big
width 100%
height 38px
line-height 38px
</style>

View File

@@ -14,7 +14,6 @@ import mediaVideo from './media-video.vue';
import notifications from './notifications.vue';
import noteForm from './post-form.vue';
import renoteForm from './renote-form.vue';
import followButton from './follow-button.vue';
import notePreview from './note-preview.vue';
import noteDetail from './note-detail.vue';
import settings from './settings.vue';
@@ -39,7 +38,6 @@ Vue.component('mk-media-video', mediaVideo);
Vue.component('mk-notifications', notifications);
Vue.component('mk-post-form', noteForm);
Vue.component('mk-renote-form', renoteForm);
Vue.component('mk-follow-button', followButton);
Vue.component('mk-note-preview', notePreview);
Vue.component('mk-note-detail', noteDetail);
Vue.component('mk-settings', settings);

View File

@@ -15,21 +15,26 @@
<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" :title="$t('click-to-tagging')">#{{ tag }}</a>
</div>
<input v-show="useCw" v-model="cw" :placeholder="$t('annotations')">
<textarea :class="{ with: (files.length != 0 || poll) }"
ref="text" v-model="text" :disabled="posting"
@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
v-autocomplete="'text'"
></textarea>
<div class="files" :class="{ with: poll }" v-show="files.length != 0">
<x-draggable :list="files" :options="{ animation: 150 }">
<div v-for="file in files" :key="file.id">
<div class="img" :style="{ backgroundImage: `url(${file.thumbnailUrl})` }" :title="file.name"></div>
<img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" :title="$t('attach-cancel')" alt=""/>
</div>
</x-draggable>
<p class="remain">{{ 4 - files.length }}/4</p>
<div class="textarea">
<textarea :class="{ with: (files.length != 0 || poll) }"
ref="text" v-model="text" :disabled="posting"
@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
v-autocomplete="'text'"
></textarea>
<button class="emoji" @click="emoji" ref="emoji">
<fa :icon="['far', 'laugh']"/>
</button>
<div class="files" :class="{ with: poll }" v-show="files.length != 0">
<x-draggable :list="files" :options="{ animation: 150 }">
<div v-for="file in files" :key="file.id">
<div class="img" :style="{ backgroundImage: `url(${file.thumbnailUrl})` }" :title="file.name"></div>
<img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" :title="$t('attach-cancel')" alt=""/>
</div>
</x-draggable>
<p class="remain">{{ 4 - files.length }}/4</p>
</div>
<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="saveDraft()"/>
</div>
<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="saveDraft()"/>
</div>
<mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
<button class="upload" :title="$t('attach-media-from-local')" @click="chooseFile"><fa icon="upload"/></button>
@@ -377,6 +382,19 @@ export default Vue.extend({
this.visibleUsers = erase(user, this.visibleUsers);
},
async emoji() {
const Picker = await import('./emoji-picker-dialog.vue').then(m => m.default);
const button = this.$refs.emoji;
const rect = button.getBoundingClientRect();
const vm = this.$root.new(Picker, {
x: button.offsetWidth + rect.left + window.pageXOffset,
y: rect.top + window.pageYOffset
});
vm.$once('chosen', emoji => {
insertTextAtCursor(this.$refs.text, emoji);
});
},
post() {
this.posting = true;
@@ -469,7 +487,7 @@ export default Vue.extend({
> .content
> input
> textarea
> .textarea > textarea
display block
width 100%
padding 12px
@@ -498,27 +516,108 @@ export default Vue.extend({
> input
margin-bottom 8px
> textarea
margin 0
max-width 100%
min-width 100%
min-height 84px
> .textarea
> .emoji
position absolute
top 0
right 0
padding 10px
font-size 18px
color var(--text)
opacity 0.5
&:hover
& + *
& + * + *
border-color var(--primaryAlpha02)
transition border-color .1s ease
&:hover
color var(--textHighlighted)
opacity 1
&:focus
& + *
& + * + *
border-color var(--primaryAlpha05)
transition border-color 0s ease
&:active
color var(--primary)
opacity 1
&.with
border-bottom solid 1px var(--primaryAlpha01) !important
border-radius 4px 4px 0 0
> textarea
margin 0
max-width 100%
min-width 100%
min-height 84px
&:hover
& + * + *
& + * + * + *
border-color var(--primaryAlpha02)
transition border-color .1s ease
&:focus
& + * + *
& + * + * + *
border-color var(--primaryAlpha05)
transition border-color 0s ease
& + .emoji
opacity 0.7
&.with
border-bottom solid 1px var(--primaryAlpha01) !important
border-radius 4px 4px 0 0
> .files
margin 0
padding 0
background var(--desktopPostFormTextareaBg)
border solid 1px var(--primaryAlpha01)
border-top none
border-radius 0 0 4px 4px
transition border-color .3s ease
&.with
border-bottom solid 1px var(--primaryAlpha01) !important
border-radius 0
> .remain
display block
position absolute
top 8px
right 8px
margin 0
padding 0
color var(--primaryAlpha04)
> div
padding 4px
&:after
content ""
display block
clear both
> div
float left
border solid 4px transparent
cursor move
&:hover > .remove
display block
> .img
width 64px
height 64px
background-size cover
background-position center center
> .remove
display none
position absolute
top -6px
right -6px
width 16px
height 16px
cursor pointer
> .mk-poll-editor
background var(--desktopPostFormTextareaBg)
border solid 1px var(--primaryAlpha01)
border-top none
border-radius 0 0 4px 4px
transition border-color .3s ease
> .visibleUsers
margin-bottom 8px
@@ -541,66 +640,6 @@ export default Vue.extend({
margin-right 8px
white-space nowrap
> .files
margin 0
padding 0
background var(--desktopPostFormTextareaBg)
border solid 1px var(--primaryAlpha01)
border-top none
border-radius 0 0 4px 4px
transition border-color .3s ease
&.with
border-bottom solid 1px var(--primaryAlpha01) !important
border-radius 0
> .remain
display block
position absolute
top 8px
right 8px
margin 0
padding 0
color var(--primaryAlpha04)
> div
padding 4px
&:after
content ""
display block
clear both
> div
float left
border solid 4px transparent
cursor move
&:hover > .remove
display block
> .img
width 64px
height 64px
background-size cover
background-position center center
> .remove
display none
position absolute
top -6px
right -6px
width 16px
height 16px
cursor pointer
> .mk-poll-editor
background var(--desktopPostFormTextareaBg)
border solid 1px var(--primaryAlpha01)
border-top none
border-radius 0 0 4px 4px
transition border-color .3s ease
> .mk-uploader
margin 8px 0 0 0
padding 8px

View File

@@ -2,7 +2,7 @@
<div class="zvdbznxvfixtmujpsigoccczftvpiwqh">
<div class="banner" :style="bannerStyle"></div>
<mk-avatar class="avatar" :user="user" :disable-preview="true"/>
<mk-follow-button :user="user" class="follow"/>
<mk-follow-button :user="user" class="follow" mini/>
<div class="body">
<router-link :to="user | userPage" class="name">{{ user | userName }}</router-link>
<span class="username">@{{ user | acct }}</span>

View File

@@ -19,7 +19,7 @@
<p>{{ $t('followers') }}</p><span>{{ u.followersCount }}</span>
</div>
</div>
<mk-follow-button v-if="$store.getters.isSignedIn && u.id != $store.state.i.id" :user="u"/>
<mk-follow-button class="follow-button" v-if="$store.getters.isSignedIn && u.id != $store.state.i.id" :user="u" mini/>
</template>
</div>
</template>
@@ -154,7 +154,7 @@ export default Vue.extend({
font-size 1em
color var(--primary)
> .mk-follow-button
> .follow-button
position absolute
top 92px
right 8px

View File

@@ -14,7 +14,7 @@
<header :style="bannerStyle">
<div>
<button class="menu" @click="menu" ref="menu"><fa icon="ellipsis-h"/></button>
<mk-follow-button v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" class="follow"/>
<mk-follow-button v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" class="follow" mini/>
<mk-avatar class="avatar" :user="user" :disable-preview="true"/>
<span class="name">{{ user | userName }}</span>
<span class="acct">@{{ user | acct }}</span>
@@ -155,7 +155,8 @@ export default Vue.extend({
this.$root.api('users/notes', {
userId: this.user.id,
fileType: image,
limit: 9
limit: 9,
untilDate: new Date().getTime() + 1000 * 86400 * 365
}).then(notes => {
notes.forEach(note => {
note.files.forEach(file => {
@@ -254,6 +255,7 @@ export default Vue.extend({
this.$root.api('users/notes', {
userId: this.user.id,
limit: fetchLimit + 1,
untilDate: new Date().getTime() + 1000 * 86400 * 365,
withFiles: this.withFiles,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
@@ -274,7 +276,7 @@ export default Vue.extend({
const promise = this.$root.api('users/notes', {
userId: this.user.id,
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id,
untilDate: new Date((this.$refs.timeline as any).tail().createdAt).getTime(),
withFiles: this.withFiles,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,

View File

@@ -9,7 +9,7 @@
<router-link class="name" :to="friend | userPage" v-user-preview="friend.id">{{ friend.name }}</router-link>
<p class="username">@{{ friend | acct }}</p>
</div>
<mk-follow-button :user="friend"/>
<mk-follow-button class="follow-button" :user="friend"/>
</div>
</template>
<p class="empty" v-if="!fetching && users.length == 0">{{ $t('no-users') }}</p>
@@ -110,7 +110,7 @@ export default Vue.extend({
color var(--text)
opacity 0.7
> .mk-follow-button
> .follow-button
position absolute
top 16px
right 16px

View File

@@ -27,7 +27,8 @@ export default Vue.extend({
this.$root.api('users/notes', {
userId: this.user.id,
withFiles: true,
limit: 9
limit: 9,
untilDate: new Date().getTime() + 1000 * 86400 * 365
}).then(notes => {
notes.forEach(note => {
note.files.forEach(file => {

View File

@@ -1,7 +1,7 @@
<template>
<div class="profile" v-if="$store.getters.isSignedIn">
<div class="friend-form" v-if="$store.state.i.id != user.id">
<mk-follow-button :user="user" size="big"/>
<mk-follow-button :user="user" block/>
<p class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</p>
<p class="stalk" v-if="user.isFollowing">
<span v-if="user.isStalking">{{ $t('stalking') }} <a @click="unstalk"><fa icon="meh"/> {{ $t('unstalk') }}</a></span>

View File

@@ -63,7 +63,7 @@ export default Vue.extend({
this.$root.api('users/notes', {
userId: this.user.id,
limit: fetchLimit + 1,
untilDate: this.date ? this.date.getTime() : undefined,
untilDate: this.date ? this.date.getTime() : new Date().getTime() + 1000 * 86400 * 365,
includeReplies: this.mode == 'with-replies',
withFiles: this.mode == 'with-media'
}).then(notes => {
@@ -86,7 +86,7 @@ export default Vue.extend({
limit: fetchLimit + 1,
includeReplies: this.mode == 'with-replies',
withFiles: this.mode == 'with-media',
untilId: (this.$refs.timeline as any).tail().id
untilDate: new Date((this.$refs.timeline as any).tail().createdAt).getTime()
});
promise.then(notes => {

View File

@@ -1,16 +1,51 @@
<template>
<div class="mkw-post-form">
<template v-if="props.design == 0">
<p class="title"><fa icon="pencil-alt"/>{{ $t('title') }}</p>
</template>
<textarea :disabled="posting" v-model="text" @keydown="onKeydown" :placeholder="placeholder"></textarea>
<button @click="post" :disabled="posting">{{ $t('note') }}</button>
<div>
<mk-widget-container :show-header="props.design == 0">
<template slot="header"><fa icon="pencil-alt"/>{{ $t('title') }}</template>
<div class="lhcuptdmcdkfwmipgazeawoiuxpzaclc-body"
@dragover.stop="onDragover"
@drop.stop="onDrop"
>
<div class="textarea">
<textarea
:disabled="posting"
v-model="text"
@keydown="onKeydown"
@paste="onPaste"
:placeholder="placeholder"
ref="text"
v-autocomplete="'text'"
></textarea>
<button class="emoji" @click="emoji" ref="emoji">
<fa :icon="['far', 'laugh']"/>
</button>
</div>
<div class="files" v-show="files.length != 0">
<x-draggable :list="files" :options="{ animation: 150 }">
<div v-for="file in files" :key="file.id">
<div class="img" :style="{ backgroundImage: `url(${file.thumbnailUrl})` }" :title="file.name"></div>
<img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" :title="$t('attach-cancel')" alt=""/>
</div>
</x-draggable>
</div>
<input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
<mk-uploader ref="uploader" @uploaded="attachMedia"/>
<footer>
<button @click="chooseFile"><fa icon="upload"/></button>
<button @click="chooseFileFromDrive"><fa icon="cloud"/></button>
<button @click="post" :disabled="posting" class="post">{{ $t('note') }}</button>
</footer>
</div>
</mk-widget-container>
</div>
</template>
<script lang="ts">
import define from '../../../common/define-widget';
import i18n from '../../../i18n';
import insertTextAtCursor from 'insert-text-at-cursor';
import * as XDraggable from 'vuedraggable';
export default define({
name: 'post-form',
@@ -19,12 +54,19 @@ export default define({
})
}).extend({
i18n: i18n('desktop/views/widgets/post-form.vue'),
components: {
XDraggable
},
data() {
return {
posting: false,
text: ''
text: '',
files: [],
};
},
computed: {
placeholder(): string {
const xs = [
@@ -38,6 +80,7 @@ export default define({
return xs[Math.floor(Math.random() * xs.length)];
}
},
methods: {
func() {
if (this.props.design == 1) {
@@ -47,14 +90,95 @@ export default define({
}
this.save();
},
chooseFile() {
(this.$refs.file as any).click();
},
chooseFileFromDrive() {
this.$chooseDriveFile({
multiple: true
}).then(files => {
files.forEach(this.attachMedia);
});
},
attachMedia(driveFile) {
this.files.push(driveFile);
this.$emit('change-attached-files', this.files);
},
detachMedia(id) {
this.files = this.files.filter(x => x.id != id);
this.$emit('change-attached-files', this.files);
},
onKeydown(e) {
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && !this.posting && this.text) this.post();
},
onPaste(e) {
Array.from(e.clipboardData.items).forEach((item: any) => {
if (item.kind == 'file') {
this.upload(item.getAsFile());
}
});
},
onChangeFile() {
Array.from((this.$refs.file as any).files).forEach(this.upload);
},
upload(file) {
(this.$refs.uploader as any).upload(file);
},
onDragover(e) {
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
if (isFile || isDriveFile) {
e.preventDefault();
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
}
},
onDrop(e): void {
// ファイルだったら
if (e.dataTransfer.files.length > 0) {
e.preventDefault();
Array.from(e.dataTransfer.files).forEach(this.upload);
return;
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData('mk_drive_file');
if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile);
this.files.push(file);
e.preventDefault();
}
//#endregion
},
async emoji() {
const Picker = await import('../components/emoji-picker-dialog.vue').then(m => m.default);
const button = this.$refs.emoji;
const rect = button.getBoundingClientRect();
const vm = this.$root.new(Picker, {
x: button.offsetWidth + rect.left + window.pageXOffset,
y: rect.top + window.pageYOffset
});
vm.$once('chosen', emoji => {
insertTextAtCursor(this.$refs.text, emoji);
});
},
post() {
this.posting = true;
this.$root.api('notes/create', {
text: this.text
text: this.text == '' ? undefined : this.text,
fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
}).then(data => {
this.clear();
}).catch(err => {
@@ -63,66 +187,114 @@ export default define({
this.posting = false;
});
},
clear() {
this.text = '';
this.files = [];
}
}
});
</script>
<style lang="stylus" scoped>
.lhcuptdmcdkfwmipgazeawoiuxpzaclc-body
> .textarea
> .emoji
position absolute
top 0
right 0
padding 10px
font-size 18px
color var(--text)
opacity 0.5
&:hover
color var(--textHighlighted)
opacity 1
.mkw-post-form
background #fff
overflow hidden
border solid 1px rgba(#000, 0.075)
border-radius 6px
&:active
color var(--primary)
opacity 1
> .title
z-index 1
margin 0
padding 0 16px
line-height 42px
font-size 0.9em
font-weight bold
color #888
box-shadow 0 1px rgba(#000, 0.07)
> textarea
display block
width 100%
max-width 100%
min-width 100%
padding 16px
color var(--desktopPostFormTextareaFg)
outline none
background var(--desktopPostFormTextareaBg)
border none
border-bottom solid 1px var(--faceDivider)
> [data-icon]
margin-right 4px
&:focus
& + .emoji
opacity 0.7
> textarea
display block
width 100%
max-width 100%
min-width 100%
padding 16px
margin-bottom 28px + 16px
border none
border-bottom solid 1px #eee
> .files
> div
padding 4px
> button
display block
position absolute
bottom 8px
right 8px
margin 0
padding 0 10px
height 28px
color var(--primaryForeground)
background var(--primary) !important
outline none
border none
border-radius 4px
transition background 0.1s ease
cursor pointer
&:after
content ""
display block
clear both
&:hover
background var(--primaryLighten10) !important
> div
float left
border solid 4px transparent
cursor move
&:active
background var(--primaryDarken10) !important
transition background 0s ease
&:hover > .remove
display block
> .img
width 64px
height 64px
background-size cover
background-position center center
> .remove
display none
position absolute
top -6px
right -6px
width 16px
height 16px
cursor pointer
> input[type=file]
display none
> footer
display flex
padding 8px
> button:not(.post)
color var(--text)
&:hover
color var(--textHighlighted)
> .post
display block
margin 0 0 0 auto
padding 0 10px
height 28px
color var(--primaryForeground)
background var(--primary) !important
outline none
border none
border-radius 4px
transition background 0.1s ease
cursor pointer
&:hover
background var(--primaryLighten10) !important
&:active
background var(--primaryDarken10) !important
transition background 0s ease
</style>

View File

@@ -114,11 +114,6 @@ export default define({
color var(--text)
opacity 0.7
> .mk-follow-button
position absolute
top 16px
right 16px
> .empty
margin 0
padding 16px

View File

@@ -1,134 +0,0 @@
<template>
<button class="mk-follow-button"
:class="{ wait: wait, active: u.isFollowing || u.hasPendingFollowRequestFromYou }"
@click="onClick"
:disabled="wait"
>
<template v-if="!wait">
<template v-if="u.hasPendingFollowRequestFromYou && u.isLocked"><fa icon="hourglass-half"/> {{ $t('request-pending') }}</template>
<template v-else-if="u.hasPendingFollowRequestFromYou && !u.isLocked"><fa icon="hourglass-start"/> {{ $t('follow-processing') }}</template>
<template v-else-if="u.isFollowing"><fa icon="minus"/> {{ $t('following') }}</template>
<template v-else-if="!u.isFollowing && u.isLocked"><fa icon="plus"/> {{ $t('follow-request') }}</template>
<template v-else-if="!u.isFollowing && !u.isLocked"><fa icon="plus"/> {{ $t('follow') }}</template>
</template>
<template v-else><fa icon="spinner .pulse" fixed-width/></template>
</button>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
export default Vue.extend({
i18n: i18n('mobile/views/components/follow-button.vue'),
props: {
user: {
type: Object,
required: true
}
},
data() {
return {
u: this.user,
wait: false,
connection: null
};
},
mounted() {
this.connection = this.$root.stream.useSharedConnection('main');
this.connection.on('follow', this.onFollowChange);
this.connection.on('unfollow', this.onFollowChange);
},
beforeDestroy() {
this.connection.dispose();
},
methods: {
onFollowChange(user) {
if (user.id == this.u.id) {
this.u.isFollowing = user.isFollowing;
this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
this.$forceUpdate();
}
},
async onClick() {
this.wait = true;
try {
if (this.u.isFollowing) {
this.u = await this.$root.api('following/delete', {
userId: this.u.id
});
} else {
if (this.u.hasPendingFollowRequestFromYou) {
this.u = await this.$root.api('following/requests/cancel', {
userId: this.u.id
});
} else if (this.u.isLocked) {
this.u = await this.$root.api('following/create', {
userId: this.u.id
});
} else {
this.u = await this.$root.api('following/create', {
userId: this.user.id
});
}
}
} catch (e) {
console.error(e);
} finally {
this.wait = false;
}
}
}
});
</script>
<style lang="stylus" scoped>
.mk-follow-button
display block
user-select none
cursor pointer
padding 0 16px
margin 0
min-width 100px
line-height 36px
font-size 14px
font-weight bold
color var(--primary)
background transparent
outline none
border solid 1px var(--primary)
border-radius 36px
&:hover
background var(--primaryAlpha01)
&:active
background var(--primaryAlpha02)
&.active
color var(--primaryForeground)
background var(--primary)
&:hover
background var(--primaryLighten10)
border-color var(--primaryLighten10)
&:active
background var(--primaryDarken10)
border-color var(--primaryDarken10)
&.wait
cursor wait !important
opacity 0.7
*
pointer-events none
</style>

View File

@@ -10,7 +10,6 @@ import subNoteContent from './sub-note-content.vue';
import noteCard from './note-card.vue';
import userCard from './user-card.vue';
import noteDetail from './note-detail.vue';
import followButton from './follow-button.vue';
import friendsMaker from './friends-maker.vue';
import notification from './notification.vue';
import notifications from './notifications.vue';
@@ -33,7 +32,6 @@ Vue.component('mk-sub-note-content', subNoteContent);
Vue.component('mk-note-card', noteCard);
Vue.component('mk-user-card', userCard);
Vue.component('mk-note-detail', noteDetail);
Vue.component('mk-follow-button', followButton);
Vue.component('mk-friends-maker', friendsMaker);
Vue.component('mk-notification', notification);
Vue.component('mk-notifications', notifications);

View File

@@ -5,7 +5,7 @@
</header>
<a class="name" :href="user | userPage" target="_blank">{{ user | userName }}</a>
<p class="username"><mk-acct :user="user"/></p>
<mk-follow-button :user="user"/>
<mk-follow-button class="follow-button" :user="user"/>
</div>
</template>
@@ -53,7 +53,7 @@ export default Vue.extend({
font-size 15px
color #ccc
> .mk-follow-button
> .follow-button
display inline-block
margin 8px 0 16px 0

View File

@@ -44,7 +44,8 @@ export default Vue.extend({
this.$root.api('users/notes', {
userId: this.user.id,
withFiles: this.withMedia,
limit: fetchLimit + 1
limit: fetchLimit + 1,
untilDate: new Date().getTime() + 1000 * 86400 * 365
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
@@ -66,7 +67,7 @@ export default Vue.extend({
userId: this.user.id,
withFiles: this.withMedia,
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id
untilDate: new Date((this.$refs.timeline as any).tail().createdAt).getTime()
});
promise.then(notes => {

View File

@@ -243,9 +243,6 @@ main
font-size 18px
color var(--text)
> .mk-follow-button
margin 0
> .title
margin 8px 0

View File

@@ -22,7 +22,8 @@ export default Vue.extend({
},
mounted() {
this.$root.api('users/notes', {
userId: this.user.id
userId: this.user.id,
untilDate: new Date().getTime() + 1000 * 86400 * 365
}).then(notes => {
this.notes = notes;
this.fetching = false;

View File

@@ -29,7 +29,8 @@ export default Vue.extend({
this.$root.api('users/notes', {
userId: this.user.id,
withFiles: true,
limit: 6
limit: 6,
untilDate: new Date().getTime() + 1000 * 86400 * 365
}).then(notes => {
notes.forEach(note => {
note.media.forEach(media => {

View File

@@ -18,6 +18,7 @@
secondary: '$secondary',
bg: ':darken<8<$secondary',
text: '$text',
textHighlighted: ':lighten<7<$text',
scrollbarTrack: ':darken<5<$secondary',
scrollbarHandle: ':lighten<5<$secondary',

View File

@@ -18,6 +18,7 @@
secondary: '$secondary',
bg: ':darken<8<$secondary',
text: '$text',
textHighlighted: ':darken<7<$text',
scrollbarTrack: '#fff',
scrollbarHandle: '#00000033',

View File

@@ -153,9 +153,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
}
//#region Construct query
const sort = {
_id: -1
};
const sort = { } as any;
const query = {
deletedAt: null,
@@ -168,15 +166,17 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
$gt: ps.sinceId
};
} else if (ps.untilId) {
sort._id = -1;
query._id = {
$lt: ps.untilId
};
} else if (ps.sinceDate) {
sort._id = 1;
sort.createdAt = 1;
query.createdAt = {
$gt: new Date(ps.sinceDate)
};
} else if (ps.untilDate) {
sort.createdAt = -1;
query.createdAt = {
$lt: new Date(ps.untilDate)
};