mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-30 04:13:09 +02:00
ページにいいねできるように
This commit is contained in:
parent
d6ccb1725b
commit
380749051d
18 changed files with 489 additions and 191 deletions
|
@ -1874,6 +1874,10 @@ pages:
|
||||||
edit-this-page: "このページを編集"
|
edit-this-page: "このページを編集"
|
||||||
view-source: "ソースを表示"
|
view-source: "ソースを表示"
|
||||||
view-page: "ページを見る"
|
view-page: "ページを見る"
|
||||||
|
like: "いいね"
|
||||||
|
unlike: "いいね解除"
|
||||||
|
liked-pages: "いいねしたページ"
|
||||||
|
my-pages: "自分のページ"
|
||||||
inspector: "インスペクター"
|
inspector: "インスペクター"
|
||||||
content: "ページブロック"
|
content: "ページブロック"
|
||||||
variables: "変数"
|
variables: "変数"
|
||||||
|
|
23
migration/1558072954435-PageLike.ts
Normal file
23
migration/1558072954435-PageLike.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||||
|
|
||||||
|
export class PageLike1558072954435 implements MigrationInterface {
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.query(`CREATE TABLE "page_like" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "pageId" character varying(32) NOT NULL, CONSTRAINT "PK_813f034843af992d3ae0f43c64c" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_0e61efab7f88dbb79c9166dbb4" ON "page_like" ("userId") `);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4ce6fb9c70529b4c8ac46c9bfa" ON "page_like" ("userId", "pageId") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "page" ADD "likedCount" integer NOT NULL DEFAULT 0`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "page_like" ADD CONSTRAINT "FK_0e61efab7f88dbb79c9166dbb48" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "page_like" ADD CONSTRAINT "FK_cf8782626dced3176038176a847" FOREIGN KEY ("pageId") REFERENCES "page"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "page_like" DROP CONSTRAINT "FK_cf8782626dced3176038176a847"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "page_like" DROP CONSTRAINT "FK_0e61efab7f88dbb79c9166dbb48"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "likedCount"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_4ce6fb9c70529b4c8ac46c9bfa"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_0e61efab7f88dbb79c9166dbb4"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "page_like"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -12,6 +12,11 @@
|
||||||
<small>@{{ page.user.username }}</small>
|
<small>@{{ page.user.username }}</small>
|
||||||
<router-link v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId" :to="`/i/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link>
|
<router-link v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId" :to="`/i/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link>
|
||||||
<router-link :to="`./${page.name}/view-source`">{{ $t('view-source') }}</router-link>
|
<router-link :to="`./${page.name}/view-source`">{{ $t('view-source') }}</router-link>
|
||||||
|
<div class="like">
|
||||||
|
<button @click="unlike()" v-if="page.isLiked" :title="$t('unlike')"><fa :icon="faHeartS"/></button>
|
||||||
|
<button @click="like()" v-else :title="$t('like')"><fa :icon="faHeart"/></button>
|
||||||
|
<span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -19,8 +24,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../../../i18n';
|
import i18n from '../../../../i18n';
|
||||||
import { faICursor, faPlus } from '@fortawesome/free-solid-svg-icons';
|
import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faSave, faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
import { faHeart } from '@fortawesome/free-regular-svg-icons';
|
||||||
import XBlock from './page.block.vue';
|
import XBlock from './page.block.vue';
|
||||||
import { ASEvaluator } from '../../../../../../misc/aiscript/evaluator';
|
import { ASEvaluator } from '../../../../../../misc/aiscript/evaluator';
|
||||||
import { collectPageVars } from '../../../scripts/collect-page-vars';
|
import { collectPageVars } from '../../../scripts/collect-page-vars';
|
||||||
|
@ -76,7 +81,7 @@ export default Vue.extend({
|
||||||
return {
|
return {
|
||||||
page: null,
|
page: null,
|
||||||
script: null,
|
script: null,
|
||||||
faPlus, faICursor, faSave, faStickyNote
|
faHeartS, faHeart
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -103,6 +108,24 @@ export default Vue.extend({
|
||||||
getPageVars() {
|
getPageVars() {
|
||||||
return collectPageVars(this.page.content);
|
return collectPageVars(this.page.content);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
like() {
|
||||||
|
this.$root.api('pages/like', {
|
||||||
|
pageId: this.page.id,
|
||||||
|
}).then(() => {
|
||||||
|
this.page.isLiked = true;
|
||||||
|
this.page.likedCount++;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
unlike() {
|
||||||
|
this.$root.api('pages/unlike', {
|
||||||
|
pageId: this.page.id,
|
||||||
|
}).then(() => {
|
||||||
|
this.page.isLiked = false;
|
||||||
|
this.page.likedCount--;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -161,4 +184,7 @@ export default Vue.extend({
|
||||||
> a + a
|
> a + a
|
||||||
margin-left 8px
|
margin-left 8px
|
||||||
|
|
||||||
|
> .like
|
||||||
|
margin-top 16px
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
138
src/client/app/common/views/pages/pages.vue
Normal file
138
src/client/app/common/views/pages/pages.vue
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ui-container :body-togglable="true">
|
||||||
|
<template #header><fa :icon="faEdit" fixed-width/>{{ $t('my-pages') }}</template>
|
||||||
|
<div class="rknalgpo" v-if="!fetching">
|
||||||
|
<ui-button class="new" @click="create()"><fa :icon="faPlus"/></ui-button>
|
||||||
|
<sequential-entrance animation="entranceFromTop" delay="25" tag="div" class="pages">
|
||||||
|
<x-page-preview v-for="page in pages" class="page" :page="page" :key="page.id"/>
|
||||||
|
</sequential-entrance>
|
||||||
|
<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
|
||||||
|
</div>
|
||||||
|
</ui-container>
|
||||||
|
|
||||||
|
<ui-container :body-togglable="true">
|
||||||
|
<template #header><fa :icon="faHeart" fixed-width/>{{ $t('liked-pages') }}</template>
|
||||||
|
<div class="rknalgpo" v-if="!fetching">
|
||||||
|
<sequential-entrance animation="entranceFromTop" delay="25" tag="div" class="pages">
|
||||||
|
<x-page-preview v-for="like in likes" class="page" :page="like.page" :key="like.page.id"/>
|
||||||
|
</sequential-entrance>
|
||||||
|
<ui-button v-if="existMoreLikes" @click="fetchMoreLiked()">{{ $t('@.load-more') }}</ui-button>
|
||||||
|
</div>
|
||||||
|
</ui-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { faPlus, faEdit } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
import i18n from '../../../i18n';
|
||||||
|
import Progress from '../../scripts/loading';
|
||||||
|
import XPagePreview from '../../views/components/page-preview.vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
i18n: i18n('pages'),
|
||||||
|
components: {
|
||||||
|
XPagePreview
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
fetching: true,
|
||||||
|
pages: [],
|
||||||
|
existMore: false,
|
||||||
|
moreFetching: false,
|
||||||
|
likes: [],
|
||||||
|
existMoreLikes: false,
|
||||||
|
moreLikesFetching: false,
|
||||||
|
faStickyNote, faPlus, faEdit, faHeart
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetch();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetch() {
|
||||||
|
Progress.start();
|
||||||
|
this.fetching = true;
|
||||||
|
|
||||||
|
const pages = await this.$root.api('i/pages', {
|
||||||
|
limit: 11
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pages.length == 11) {
|
||||||
|
this.existMore = true;
|
||||||
|
pages.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const likes = await this.$root.api('i/page-likes', {
|
||||||
|
limit: 11
|
||||||
|
});
|
||||||
|
|
||||||
|
if (likes.length == 11) {
|
||||||
|
this.existMoreLikes = true;
|
||||||
|
likes.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pages = pages;
|
||||||
|
this.likes = likes;
|
||||||
|
this.fetching = false;
|
||||||
|
|
||||||
|
Progress.done();
|
||||||
|
},
|
||||||
|
fetchMore() {
|
||||||
|
this.moreFetching = true;
|
||||||
|
this.$root.api('i/pages', {
|
||||||
|
limit: 11,
|
||||||
|
untilId: this.pages[this.pages.length - 1].id
|
||||||
|
}).then(pages => {
|
||||||
|
if (pages.length == 11) {
|
||||||
|
this.existMore = true;
|
||||||
|
pages.pop();
|
||||||
|
} else {
|
||||||
|
this.existMore = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pages = this.pages.concat(pages);
|
||||||
|
this.moreFetching = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchMoreLiked() {
|
||||||
|
this.moreLikesFetching = true;
|
||||||
|
this.$root.api('i/page-likes', {
|
||||||
|
limit: 11,
|
||||||
|
untilId: this.likes[this.likes.length - 1].id
|
||||||
|
}).then(pages => {
|
||||||
|
if (pages.length == 11) {
|
||||||
|
this.existMoreLikes = true;
|
||||||
|
pages.pop();
|
||||||
|
} else {
|
||||||
|
this.existMoreLikes = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.likes = this.likes.concat(pages);
|
||||||
|
this.moreLikesFetching = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
create() {
|
||||||
|
this.$router.push(`/i/pages/new`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.rknalgpo
|
||||||
|
padding 16px
|
||||||
|
|
||||||
|
> .new
|
||||||
|
margin-bottom 16px
|
||||||
|
|
||||||
|
> * > .page
|
||||||
|
margin-bottom 8px
|
||||||
|
|
||||||
|
@media (min-width 500px)
|
||||||
|
> * > .page
|
||||||
|
margin-bottom 16px
|
||||||
|
|
||||||
|
</style>
|
|
@ -156,7 +156,7 @@ init(async (launch, os) => {
|
||||||
{ path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
|
{ path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
|
||||||
{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
|
{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
|
||||||
{ path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) },
|
{ path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) },
|
||||||
{ path: '/i/pages', component: () => import('./views/home/pages.vue').then(m => m.default) },
|
{ path: '/i/pages', component: () => import('../common/views/pages/pages.vue').then(m => m.default) },
|
||||||
]},
|
]},
|
||||||
{ path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) },
|
{ path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) },
|
||||||
{ path: '/@:user/pages/:pageName/view-source', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
|
{ path: '/@:user/pages/:pageName/view-source', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="rknalgpo" v-if="!fetching">
|
|
||||||
<ui-button @click="create()"><fa :icon="faPlus"/></ui-button>
|
|
||||||
<sequential-entrance animation="entranceFromTop" delay="25">
|
|
||||||
<template v-for="page in pages">
|
|
||||||
<x-page-preview class="page" :page="page" :key="page.id"/>
|
|
||||||
</template>
|
|
||||||
</sequential-entrance>
|
|
||||||
<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Vue from 'vue';
|
|
||||||
import i18n from '../../../i18n';
|
|
||||||
import Progress from '../../../common/scripts/loading';
|
|
||||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
|
||||||
import XPagePreview from '../../../common/views/components/page-preview.vue';
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
i18n: i18n(),
|
|
||||||
components: {
|
|
||||||
XPagePreview
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
fetching: true,
|
|
||||||
pages: [],
|
|
||||||
existMore: false,
|
|
||||||
moreFetching: false,
|
|
||||||
faStickyNote, faPlus
|
|
||||||
};
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.fetch();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetch() {
|
|
||||||
Progress.start();
|
|
||||||
this.fetching = true;
|
|
||||||
|
|
||||||
this.$root.api('i/pages', {
|
|
||||||
limit: 11
|
|
||||||
}).then(pages => {
|
|
||||||
if (pages.length == 11) {
|
|
||||||
this.existMore = true;
|
|
||||||
pages.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pages = pages;
|
|
||||||
this.fetching = false;
|
|
||||||
|
|
||||||
Progress.done();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
fetchMore() {
|
|
||||||
this.moreFetching = true;
|
|
||||||
this.$root.api('i/pages', {
|
|
||||||
limit: 11,
|
|
||||||
untilId: this.pages[this.pages.length - 1].id
|
|
||||||
}).then(pages => {
|
|
||||||
if (pages.length == 11) {
|
|
||||||
this.existMore = true;
|
|
||||||
pages.pop();
|
|
||||||
} else {
|
|
||||||
this.existMore = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pages = this.pages.concat(pages);
|
|
||||||
this.moreFetching = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
create() {
|
|
||||||
this.$router.push(`/i/pages/new`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
|
||||||
.rknalgpo
|
|
||||||
margin 0 auto
|
|
||||||
|
|
||||||
> * > .page
|
|
||||||
margin-bottom 8px
|
|
||||||
|
|
||||||
@media (min-width 500px)
|
|
||||||
> * > .page
|
|
||||||
margin-bottom 16px
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -3,92 +3,27 @@
|
||||||
<template #header><span style="margin-right:4px;"><fa :icon="faStickyNote"/></span>{{ $t('@.pages') }}</template>
|
<template #header><span style="margin-right:4px;"><fa :icon="faStickyNote"/></span>{{ $t('@.pages') }}</template>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<ui-button @click="create()"><fa :icon="faPlus"/></ui-button>
|
<x-pages v-bind="$attrs"/>
|
||||||
<sequential-entrance animation="entranceFromTop" delay="25">
|
|
||||||
<template v-for="page in pages">
|
|
||||||
<x-page-preview class="page" :page="page" :key="page.id"/>
|
|
||||||
</template>
|
|
||||||
</sequential-entrance>
|
|
||||||
<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
|
|
||||||
</main>
|
</main>
|
||||||
</mk-ui>
|
</mk-ui>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
import Progress from '../../../common/scripts/loading';
|
import { faHashtag } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
import XPages from '../../../common/views/pages/pages.vue';
|
||||||
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
|
||||||
import XPagePreview from '../../../common/views/components/page-preview.vue';
|
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n(),
|
i18n: i18n(''),
|
||||||
components: {
|
components: {
|
||||||
XPagePreview
|
XPages
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
fetching: true,
|
faHashtag
|
||||||
pages: [],
|
|
||||||
existMore: false,
|
|
||||||
moreFetching: false,
|
|
||||||
faStickyNote, faPlus
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
|
||||||
this.fetch();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetch() {
|
|
||||||
Progress.start();
|
|
||||||
this.fetching = true;
|
|
||||||
|
|
||||||
this.$root.api('i/pages', {
|
|
||||||
limit: 11
|
|
||||||
}).then(pages => {
|
|
||||||
if (pages.length == 11) {
|
|
||||||
this.existMore = true;
|
|
||||||
pages.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pages = pages;
|
|
||||||
this.fetching = false;
|
|
||||||
|
|
||||||
Progress.done();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
fetchMore() {
|
|
||||||
this.moreFetching = true;
|
|
||||||
this.$root.api('i/pages', {
|
|
||||||
limit: 11,
|
|
||||||
untilId: this.pages[this.pages.length - 1].id
|
|
||||||
}).then(pages => {
|
|
||||||
if (pages.length == 11) {
|
|
||||||
this.existMore = true;
|
|
||||||
pages.pop();
|
|
||||||
} else {
|
|
||||||
this.existMore = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pages = this.pages.concat(pages);
|
|
||||||
this.moreFetching = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
create() {
|
|
||||||
this.$router.push(`/i/pages/new`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
|
||||||
main
|
|
||||||
> * > .page
|
|
||||||
margin-bottom 8px
|
|
||||||
|
|
||||||
@media (min-width 500px)
|
|
||||||
> * > .page
|
|
||||||
margin-bottom 16px
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ import { UserKeypair } from '../models/entities/user-keypair';
|
||||||
import { UserPublickey } from '../models/entities/user-publickey';
|
import { UserPublickey } from '../models/entities/user-publickey';
|
||||||
import { UserProfile } from '../models/entities/user-profile';
|
import { UserProfile } from '../models/entities/user-profile';
|
||||||
import { Page } from '../models/entities/page';
|
import { Page } from '../models/entities/page';
|
||||||
|
import { PageLike } from '../models/entities/page-like';
|
||||||
|
|
||||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
||||||
|
|
||||||
|
@ -116,6 +117,7 @@ export function initDb(justBorrow = false, sync = false, log = false) {
|
||||||
NoteWatching,
|
NoteWatching,
|
||||||
NoteUnread,
|
NoteUnread,
|
||||||
Page,
|
Page,
|
||||||
|
PageLike,
|
||||||
Log,
|
Log,
|
||||||
DriveFile,
|
DriveFile,
|
||||||
DriveFolder,
|
DriveFolder,
|
||||||
|
|
33
src/models/entities/page-like.ts
Normal file
33
src/models/entities/page-like.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||||
|
import { User } from './user';
|
||||||
|
import { id } from '../id';
|
||||||
|
import { Page } from './page';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Index(['userId', 'pageId'], { unique: true })
|
||||||
|
export class PageLike {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@Column('timestamp with time zone')
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column(id())
|
||||||
|
public userId: User['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => User, {
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public user: User | null;
|
||||||
|
|
||||||
|
@Column(id())
|
||||||
|
public pageId: Page['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => Page, {
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public page: Page | null;
|
||||||
|
}
|
|
@ -95,6 +95,11 @@ export class Page {
|
||||||
})
|
})
|
||||||
public visibleUserIds: User['id'][];
|
public visibleUserIds: User['id'][];
|
||||||
|
|
||||||
|
@Column('integer', {
|
||||||
|
default: 0
|
||||||
|
})
|
||||||
|
public likedCount: number;
|
||||||
|
|
||||||
constructor(data: Partial<Page>) {
|
constructor(data: Partial<Page>) {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ import { AuthSessionRepository } from './repositories/auth-session';
|
||||||
import { UserProfile } from './entities/user-profile';
|
import { UserProfile } from './entities/user-profile';
|
||||||
import { HashtagRepository } from './repositories/hashtag';
|
import { HashtagRepository } from './repositories/hashtag';
|
||||||
import { PageRepository } from './repositories/page';
|
import { PageRepository } from './repositories/page';
|
||||||
|
import { PageLikeRepository } from './repositories/page-like';
|
||||||
|
|
||||||
export const Apps = getCustomRepository(AppRepository);
|
export const Apps = getCustomRepository(AppRepository);
|
||||||
export const Notes = getCustomRepository(NoteRepository);
|
export const Notes = getCustomRepository(NoteRepository);
|
||||||
|
@ -74,3 +75,4 @@ export const ReversiGames = getCustomRepository(ReversiGameRepository);
|
||||||
export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
|
export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
|
||||||
export const Logs = getRepository(Log);
|
export const Logs = getRepository(Log);
|
||||||
export const Pages = getCustomRepository(PageRepository);
|
export const Pages = getCustomRepository(PageRepository);
|
||||||
|
export const PageLikes = getCustomRepository(PageLikeRepository);
|
||||||
|
|
26
src/models/repositories/page-like.ts
Normal file
26
src/models/repositories/page-like.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { EntityRepository, Repository } from 'typeorm';
|
||||||
|
import { PageLike } from '../entities/page-like';
|
||||||
|
import { Pages } from '..';
|
||||||
|
import { ensure } from '../../prelude/ensure';
|
||||||
|
|
||||||
|
@EntityRepository(PageLike)
|
||||||
|
export class PageLikeRepository extends Repository<PageLike> {
|
||||||
|
public async pack(
|
||||||
|
src: PageLike['id'] | PageLike,
|
||||||
|
me?: any
|
||||||
|
) {
|
||||||
|
const like = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: like.id,
|
||||||
|
page: await Pages.pack(like.page || like.pageId, me),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public packMany(
|
||||||
|
likes: any[],
|
||||||
|
me: any
|
||||||
|
) {
|
||||||
|
return Promise.all(likes.map(x => this.pack(x, me)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,24 +1,30 @@
|
||||||
import { EntityRepository, Repository } from 'typeorm';
|
import { EntityRepository, Repository } from 'typeorm';
|
||||||
import { Page } from '../entities/page';
|
import { Page } from '../entities/page';
|
||||||
import { SchemaType, types, bool } from '../../misc/schema';
|
import { SchemaType, types, bool } from '../../misc/schema';
|
||||||
import { Users, DriveFiles } from '..';
|
import { Users, DriveFiles, PageLikes } from '..';
|
||||||
import { awaitAll } from '../../prelude/await-all';
|
import { awaitAll } from '../../prelude/await-all';
|
||||||
import { DriveFile } from '../entities/drive-file';
|
import { DriveFile } from '../entities/drive-file';
|
||||||
|
import { User } from '../entities/user';
|
||||||
|
import { ensure } from '../../prelude/ensure';
|
||||||
|
|
||||||
export type PackedPage = SchemaType<typeof packedPageSchema>;
|
export type PackedPage = SchemaType<typeof packedPageSchema>;
|
||||||
|
|
||||||
@EntityRepository(Page)
|
@EntityRepository(Page)
|
||||||
export class PageRepository extends Repository<Page> {
|
export class PageRepository extends Repository<Page> {
|
||||||
public async pack(
|
public async pack(
|
||||||
src: Page,
|
src: Page['id'] | Page,
|
||||||
|
me?: User['id'] | User | null | undefined,
|
||||||
): Promise<PackedPage> {
|
): Promise<PackedPage> {
|
||||||
|
const meId = me ? typeof me === 'string' ? me : me.id : null;
|
||||||
|
const page = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
|
||||||
|
|
||||||
const attachedFiles: Promise<DriveFile | undefined>[] = [];
|
const attachedFiles: Promise<DriveFile | undefined>[] = [];
|
||||||
const collectFile = (xs: any[]) => {
|
const collectFile = (xs: any[]) => {
|
||||||
for (const x of xs) {
|
for (const x of xs) {
|
||||||
if (x.type === 'image') {
|
if (x.type === 'image') {
|
||||||
attachedFiles.push(DriveFiles.findOne({
|
attachedFiles.push(DriveFiles.findOne({
|
||||||
id: x.fileId,
|
id: x.fileId,
|
||||||
userId: src.userId
|
userId: page.userId
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (x.children) {
|
if (x.children) {
|
||||||
|
@ -26,7 +32,7 @@ export class PageRepository extends Repository<Page> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
collectFile(src.content);
|
collectFile(page.content);
|
||||||
|
|
||||||
// 後方互換性のため
|
// 後方互換性のため
|
||||||
let migrated = false;
|
let migrated = false;
|
||||||
|
@ -47,29 +53,31 @@ export class PageRepository extends Repository<Page> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
migrate(src.content);
|
migrate(page.content);
|
||||||
if (migrated) {
|
if (migrated) {
|
||||||
this.update(src.id, {
|
this.update(page.id, {
|
||||||
content: src.content
|
content: page.content
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return await awaitAll({
|
return await awaitAll({
|
||||||
id: src.id,
|
id: page.id,
|
||||||
createdAt: src.createdAt.toISOString(),
|
createdAt: page.createdAt.toISOString(),
|
||||||
updatedAt: src.updatedAt.toISOString(),
|
updatedAt: page.updatedAt.toISOString(),
|
||||||
userId: src.userId,
|
userId: page.userId,
|
||||||
user: Users.pack(src.user || src.userId),
|
user: Users.pack(page.user || page.userId),
|
||||||
content: src.content,
|
content: page.content,
|
||||||
variables: src.variables,
|
variables: page.variables,
|
||||||
title: src.title,
|
title: page.title,
|
||||||
name: src.name,
|
name: page.name,
|
||||||
summary: src.summary,
|
summary: page.summary,
|
||||||
alignCenter: src.alignCenter,
|
alignCenter: page.alignCenter,
|
||||||
font: src.font,
|
font: page.font,
|
||||||
eyeCatchingImageId: src.eyeCatchingImageId,
|
eyeCatchingImageId: page.eyeCatchingImageId,
|
||||||
eyeCatchingImage: src.eyeCatchingImageId ? await DriveFiles.pack(src.eyeCatchingImageId) : null,
|
eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null,
|
||||||
attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles))
|
attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)),
|
||||||
|
likedCount: page.likedCount,
|
||||||
|
isLiked: meId ? await PageLikes.findOne({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
45
src/server/api/endpoints/i/page-likes.ts
Normal file
45
src/server/api/endpoints/i/page-likes.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import $ from 'cafy';
|
||||||
|
import { ID } from '../../../../misc/cafy-id';
|
||||||
|
import define from '../../define';
|
||||||
|
import { PageLikes } from '../../../../models';
|
||||||
|
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '「いいね」したページ一覧を取得します。',
|
||||||
|
'en-US': 'Get liked pages'
|
||||||
|
},
|
||||||
|
|
||||||
|
tags: ['account', 'pages'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'read:page-likes',
|
||||||
|
|
||||||
|
params: {
|
||||||
|
limit: {
|
||||||
|
validator: $.optional.num.range(1, 100),
|
||||||
|
default: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
sinceId: {
|
||||||
|
validator: $.optional.type(ID),
|
||||||
|
},
|
||||||
|
|
||||||
|
untilId: {
|
||||||
|
validator: $.optional.type(ID),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async (ps, user) => {
|
||||||
|
const query = makePaginationQuery(PageLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId)
|
||||||
|
.andWhere(`like.userId = :meId`, { meId: user.id })
|
||||||
|
.leftJoinAndSelect('like.page', 'page');
|
||||||
|
|
||||||
|
const likes = await query
|
||||||
|
.take(ps.limit!)
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
return await PageLikes.packMany(likes, user);
|
||||||
|
});
|
79
src/server/api/endpoints/pages/like.ts
Normal file
79
src/server/api/endpoints/pages/like.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import $ from 'cafy';
|
||||||
|
import { ID } from '../../../../misc/cafy-id';
|
||||||
|
import define from '../../define';
|
||||||
|
import { ApiError } from '../../error';
|
||||||
|
import { Pages, PageLikes } from '../../../../models';
|
||||||
|
import { genId } from '../../../../misc/gen-id';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '指定したページを「いいね」します。',
|
||||||
|
},
|
||||||
|
|
||||||
|
tags: ['pages'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'write:page-likes',
|
||||||
|
|
||||||
|
params: {
|
||||||
|
pageId: {
|
||||||
|
validator: $.type(ID),
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '対象のページのID',
|
||||||
|
'en-US': 'Target page ID.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchPage: {
|
||||||
|
message: 'No such page.',
|
||||||
|
code: 'NO_SUCH_PAGE',
|
||||||
|
id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3'
|
||||||
|
},
|
||||||
|
|
||||||
|
yourPage: {
|
||||||
|
message: 'You cannot like your page.',
|
||||||
|
code: 'YOUR_PAGE',
|
||||||
|
id: '28800466-e6db-40f2-8fae-bf9e82aa92b8'
|
||||||
|
},
|
||||||
|
|
||||||
|
alreadyLiked: {
|
||||||
|
message: 'The page has already been liked.',
|
||||||
|
code: 'ALREADY_LIKED',
|
||||||
|
id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async (ps, user) => {
|
||||||
|
const page = await Pages.findOne(ps.pageId);
|
||||||
|
if (page == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page.userId === user.id) {
|
||||||
|
throw new ApiError(meta.errors.yourPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if already liked
|
||||||
|
const exist = await PageLikes.findOne({
|
||||||
|
pageId: page.id,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exist != null) {
|
||||||
|
throw new ApiError(meta.errors.alreadyLiked);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create like
|
||||||
|
await PageLikes.save({
|
||||||
|
id: genId(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
pageId: page.id,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
Pages.increment({ id: page.id }, 'likedCount', 1);
|
||||||
|
});
|
|
@ -70,5 +70,5 @@ export default define(meta, async (ps, user) => {
|
||||||
throw new ApiError(meta.errors.noSuchPage);
|
throw new ApiError(meta.errors.noSuchPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await Pages.pack(page);
|
return await Pages.pack(page, user);
|
||||||
});
|
});
|
||||||
|
|
62
src/server/api/endpoints/pages/unlike.ts
Normal file
62
src/server/api/endpoints/pages/unlike.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import $ from 'cafy';
|
||||||
|
import { ID } from '../../../../misc/cafy-id';
|
||||||
|
import define from '../../define';
|
||||||
|
import { ApiError } from '../../error';
|
||||||
|
import { Pages, PageLikes } from '../../../../models';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '指定したページの「いいね」を解除します。',
|
||||||
|
},
|
||||||
|
|
||||||
|
tags: ['pages'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'write:page-likes',
|
||||||
|
|
||||||
|
params: {
|
||||||
|
pageId: {
|
||||||
|
validator: $.type(ID),
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '対象のページのID',
|
||||||
|
'en-US': 'Target page ID.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchPage: {
|
||||||
|
message: 'No such page.',
|
||||||
|
code: 'NO_SUCH_PAGE',
|
||||||
|
id: 'a0d41e20-1993-40bd-890e-f6e560ae648e'
|
||||||
|
},
|
||||||
|
|
||||||
|
notLiked: {
|
||||||
|
message: 'You have not liked that page.',
|
||||||
|
code: 'NOT_LIKED',
|
||||||
|
id: 'f5e586b0-ce93-4050-b0e3-7f31af5259ee'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async (ps, user) => {
|
||||||
|
const page = await Pages.findOne(ps.pageId);
|
||||||
|
if (page == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exist = await PageLikes.findOne({
|
||||||
|
pageId: page.id,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exist == null) {
|
||||||
|
throw new ApiError(meta.errors.notLiked);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete like
|
||||||
|
await PageLikes.delete(exist.id);
|
||||||
|
|
||||||
|
Pages.decrement({ id: page.id }, 'likedCount', 1);
|
||||||
|
});
|
|
@ -21,4 +21,6 @@ export const kinds = [
|
||||||
'write:votes',
|
'write:votes',
|
||||||
'read:pages',
|
'read:pages',
|
||||||
'write:pages',
|
'write:pages',
|
||||||
|
'write:page-likes',
|
||||||
|
'read:page-likes',
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in a new issue