diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ab867e57f..d387bff55e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ChangeLog (Release Notes) ========================= 主に notable な changes を書いていきます +unreleased +----------------- +* ホームのカスタマイズを実装するなど + 2971 (2017/11/08) ----------------- * バグ修正 diff --git a/package.json b/package.json index b13d17e1fb..62006e0671 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "autwh": "0.0.1", "bcryptjs": "2.4.3", "body-parser": "1.18.2", - "cafy": "3.1.1", + "cafy": "3.2.0", "chalk": "2.3.0", "compression": "1.7.1", "cors": "2.8.4", @@ -142,6 +142,7 @@ "rndstr": "1.0.0", "s-age": "1.1.0", "serve-favicon": "2.4.5", + "sortablejs": "1.7.0", "summaly": "2.0.3", "syuilo-password-strength": "0.0.1", "tcp-port-used": "0.1.2", diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index afefce39e5..2783c92027 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -159,6 +159,11 @@ const endpoints: Endpoint[] = [ }, kind: 'account-write' }, + { + name: 'i/update_home', + withCredential: true, + kind: 'account-write' + }, { name: 'i/change_password', withCredential: true diff --git a/src/api/endpoints/i/appdata/get.ts b/src/api/endpoints/i/appdata/get.ts index a1a57fa13a..571208d46c 100644 --- a/src/api/endpoints/i/appdata/get.ts +++ b/src/api/endpoints/i/appdata/get.ts @@ -13,38 +13,27 @@ import Appdata from '../../../models/appdata'; * @param {Boolean} isSecure * @return {Promise} */ -module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) => { +module.exports = (params, user, app) => new Promise(async (res, rej) => { + if (app == null) return rej('このAPIはサードパーティAppからのみ利用できます'); + // Get 'key' parameter const [key = null, keyError] = $(params.key).optional.nullable.string().match(/[a-z_]+/).$; if (keyError) return rej('invalid key param'); - if (isSecure) { - if (!user.data) { - return res(); - } - if (key !== null) { - const data = {}; - data[key] = user.data[key]; - res(data); - } else { - res(user.data); - } - } else { - const select = {}; - if (key !== null) { - select[`data.${key}`] = true; - } - const appdata = await Appdata.findOne({ - app_id: app._id, - user_id: user._id - }, { - fields: select - }); + const select = {}; + if (key !== null) { + select[`data.${key}`] = true; + } + const appdata = await Appdata.findOne({ + app_id: app._id, + user_id: user._id + }, { + fields: select + }); - if (appdata) { - res(appdata.data); - } else { - res(); - } + if (appdata) { + res(appdata.data); + } else { + res(); } }); diff --git a/src/api/endpoints/i/appdata/set.ts b/src/api/endpoints/i/appdata/set.ts index 9c3dbe185b..2804a14cb3 100644 --- a/src/api/endpoints/i/appdata/set.ts +++ b/src/api/endpoints/i/appdata/set.ts @@ -3,9 +3,6 @@ */ import $ from 'cafy'; import Appdata from '../../../models/appdata'; -import User from '../../../models/user'; -import serialize from '../../../serializers/user'; -import event from '../../../event'; /** * Set app data @@ -16,7 +13,9 @@ import event from '../../../event'; * @param {Boolean} isSecure * @return {Promise} */ -module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) => { +module.exports = (params, user, app) => new Promise(async (res, rej) => { + if (app == null) return rej('このAPIはサードパーティAppからのみ利用できます'); + // Get 'data' parameter const [data, dataError] = $(params.data).optional.object() .pipe(obj => { @@ -43,31 +42,17 @@ module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) = set[`data.${key}`] = value; } - if (isSecure) { - const _user = await User.findOneAndUpdate(user._id, { + await Appdata.update({ + app_id: app._id, + user_id: user._id + }, Object.assign({ + app_id: app._id, + user_id: user._id + }, { $set: set + }), { + upsert: true }); - res(204); - - // Publish i updated event - event(user._id, 'i_updated', await serialize(_user, user, { - detail: true, - includeSecrets: true - })); - } else { - await Appdata.update({ - app_id: app._id, - user_id: user._id - }, Object.assign({ - app_id: app._id, - user_id: user._id - }, { - $set: set - }), { - upsert: true - }); - - res(204); - } + res(204); }); diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts index 111a4b1909..c484c51a96 100644 --- a/src/api/endpoints/i/update.ts +++ b/src/api/endpoints/i/update.ts @@ -48,13 +48,19 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re if (bannerIdErr) return rej('invalid banner_id param'); if (bannerId) user.banner_id = bannerId; + // Get 'show_donation' parameter + const [showDonation, showDonationErr] = $(params.show_donation).optional.boolean().$; + if (showDonationErr) return rej('invalid show_donation param'); + if (showDonation) user.client_settings.show_donation = showDonation; + await User.update(user._id, { $set: { name: user.name, description: user.description, avatar_id: user.avatar_id, banner_id: user.banner_id, - profile: user.profile + profile: user.profile, + 'client_settings.show_donation': user.client_settings.show_donation } }); diff --git a/src/api/endpoints/i/update_home.ts b/src/api/endpoints/i/update_home.ts new file mode 100644 index 0000000000..b9a7642b8e --- /dev/null +++ b/src/api/endpoints/i/update_home.ts @@ -0,0 +1,34 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import User from '../../models/user'; + +/** + * Update myself + * + * @param {any} params + * @param {any} user + * @param {any} _ + * @param {boolean} isSecure + * @return {Promise} + */ +module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => { + // Get 'home' parameter + const [home, homeErr] = $(params.home).array().each( + $().strict.object() + .have('name', $().string()) + .have('id', $().string()) + .have('place', $().string()) + .have('data', $().object())).$; + if (homeErr) return rej('invalid home param'); + + await User.update(user._id, { + $set: { + 'client_settings.home': home + } + }); + + // Send response + res(); +}); diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts index 7a6b9c098e..e24734f80c 100644 --- a/src/api/private/signup.ts +++ b/src/api/private/signup.ts @@ -1,3 +1,4 @@ +import * as uuid from 'uuid'; import * as express from 'express'; import * as bcrypt from 'bcryptjs'; import recaptcha = require('recaptcha-promise'); @@ -11,6 +12,28 @@ recaptcha.init({ secret_key: config.recaptcha.secretKey }); +const home = { + left: [ + 'profile', + 'calendar', + 'activity', + 'rss-reader', + 'trends', + 'photo-stream', + 'version' + ], + right: [ + 'broadcast', + 'notifications', + 'user-recommendation', + 'recommended-polls', + 'server', + 'donation', + 'nav', + 'tips' + ] +}; + export default async (req: express.Request, res: express.Response) => { // Verify recaptcha // ただしテスト時はこの機構は障害となるため無効にする @@ -60,6 +83,28 @@ export default async (req: express.Request, res: express.Response) => { // Generate secret const secret = generateUserToken(); + //#region Construct home data + const homeData = []; + + home.left.forEach(widget => { + homeData.push({ + name: widget, + id: uuid(), + place: 'left', + data: {} + }); + }); + + home.right.forEach(widget => { + homeData.push({ + name: widget, + id: uuid(), + place: 'right', + data: {} + }); + }); + //#endregion + // Create account const account: IUser = await User.insert({ token: secret, @@ -88,6 +133,11 @@ export default async (req: express.Request, res: express.Response) => { height: null, location: null, weight: null + }, + settings: {}, + client_settings: { + home: homeData, + show_donation: false } }); diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts index 0d24d6cc04..3d84156606 100644 --- a/src/api/serializers/user.ts +++ b/src/api/serializers/user.ts @@ -35,9 +35,10 @@ export default ( let _user: any; const fields = opts.detail ? { - data: false + settings: false } : { - data: false, + settings: false, + client_settings: false, profile: false, keywords: false, domains: false @@ -72,7 +73,7 @@ export default ( delete _user._id; // Remove needless properties - delete _user.lates_post; + delete _user.latest_post; // Remove private properties delete _user.password; @@ -86,8 +87,8 @@ export default ( // Visible via only the official client if (!opts.includeSecrets) { - delete _user.data; delete _user.email; + delete _user.client_settings; } _user.avatar_url = _user.avatar_id != null diff --git a/src/web/app/common/scripts/generate-default-userdata.js b/src/web/app/common/scripts/generate-default-userdata.js deleted file mode 100644 index 1200563e1a..0000000000 --- a/src/web/app/common/scripts/generate-default-userdata.js +++ /dev/null @@ -1,45 +0,0 @@ -import uuid from './uuid'; - -const home = { - left: [ - 'profile', - 'calendar', - 'rss-reader', - 'photo-stream', - 'version' - ], - right: [ - 'broadcast', - 'notifications', - 'user-recommendation', - 'donation', - 'nav', - 'tips' - ] -}; - -export default () => { - const homeData = []; - - home.left.forEach(widget => { - homeData.push({ - name: widget, - id: uuid(), - place: 'left' - }); - }); - - home.right.forEach(widget => { - homeData.push({ - name: widget, - id: uuid(), - place: 'right' - }); - }); - - const data = { - home: JSON.stringify(homeData) - }; - - return data; -}; diff --git a/src/web/app/desktop/router.js b/src/web/app/desktop/router.js index 104b9bbe5d..bbd68cf99a 100644 --- a/src/web/app/desktop/router.js +++ b/src/web/app/desktop/router.js @@ -9,6 +9,7 @@ let page = null; export default me => { route('/', index); route('/selectdrive', selectDrive); + route('/i/customize-home', customizeHome); route('/i/drive', drive); route('/i/drive/folder/:folder', drive); route('/i/mentions', mentions); @@ -27,6 +28,10 @@ export default me => { mount(document.createElement('mk-home-page')); } + function customizeHome() { + mount(document.createElement('mk-home-customize-page')); + } + function entrance() { mount(document.createElement('mk-entrance')); document.documentElement.setAttribute('data-page', 'entrance'); diff --git a/src/web/app/desktop/tags/donation.tag b/src/web/app/desktop/tags/donation.tag index 33f377a192..1c19fac1f5 100644 --- a/src/web/app/desktop/tags/donation.tag +++ b/src/web/app/desktop/tags/donation.tag @@ -54,11 +54,10 @@ e.preventDefault(); e.stopPropagation(); - this.I.data.no_donation = 'true'; + this.I.client_settings.show_donation = false; this.I.update(); - this.api('i/appdata/set', { - key: 'no_donation', - value: 'true' + this.api('i/update', { + show_donation: false }); this.unmount(); diff --git a/src/web/app/desktop/tags/home-widgets/activity.tag b/src/web/app/desktop/tags/home-widgets/activity.tag index 8bd8bfb2aa..7bdcd334c9 100644 --- a/src/web/app/desktop/tags/home-widgets/activity.tag +++ b/src/web/app/desktop/tags/home-widgets/activity.tag @@ -54,7 +54,7 @@ this.mixin('api'); this.initializing = true; - this.view = 0; + this.view = this.opts.data.hasOwnProperty('view') ? this.opts.data.view : 0; this.on('mount', () => { this.api('aggregation/users/activity', { @@ -71,6 +71,14 @@ this.toggle = () => { this.view++; if (this.view == 2) this.view = 0; + + // Save view state + this.I.client_settings.home.filter(w => w.id == this.opts.id)[0].data.view = this.view; + this.api('i/update_home', { + home: this.I.client_settings.home + }).then(() => { + this.I.update(); + }); }; diff --git a/src/web/app/desktop/tags/home-widgets/server.tag b/src/web/app/desktop/tags/home-widgets/server.tag index bc8f313d53..235867c38a 100644 --- a/src/web/app/desktop/tags/home-widgets/server.tag +++ b/src/web/app/desktop/tags/home-widgets/server.tag @@ -56,10 +56,11 @@ diff --git a/src/web/app/desktop/tags/home.tag b/src/web/app/desktop/tags/home.tag index 37b2d3cf7e..f0c71a7ea8 100644 --- a/src/web/app/desktop/tags/home.tag +++ b/src/web/app/desktop/tags/home.tag @@ -1,4 +1,30 @@ - + +
+
+

ウィジェットを追加:

+ + +
+
+

ゴミ箱 (ここにウィジェットをドロップすると削除できます)

+
+
@@ -11,25 +37,37 @@ :scope display block + &:not([data-customize]) + > .main > *:empty + display none + + > .customize + display flex + margin 0 auto + max-width 1200px + background #fff1c8 + + > div + width 50% + + &.trash + background #ffc5c5 + > .main + display flex + justify-content center margin 0 auto max-width 1200px - &:after - content "" - display block - clear both - > * - float left - - > * + > *:not(.customize-container) + > .customize-container > * display block - //border solid 1px #eaeaea border solid 1px rgba(0, 0, 0, 0.075) border-radius 6px - //box-shadow 0px 2px 16px rgba(0, 0, 0, 0.2) + > *:not(.customize-container) + > .customize-container &:not(:last-child) margin-bottom 16px @@ -40,6 +78,12 @@ > *:not(main) width 275px + > .customize-container + cursor move + + > * + pointer-events none + > .left padding 16px 0 16px 16px @@ -58,66 +102,49 @@ diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js index 7997bcc7f2..c36a06e499 100644 --- a/src/web/app/desktop/tags/index.js +++ b/src/web/app/desktop/tags/index.js @@ -57,6 +57,7 @@ require('./pages/entrance.tag'); require('./pages/entrance/signin.tag'); require('./pages/entrance/signup.tag'); require('./pages/home.tag'); +require('./pages/home-customize.tag'); require('./pages/user.tag'); require('./pages/post.tag'); require('./pages/search.tag'); diff --git a/src/web/app/desktop/tags/pages/home-customize.tag b/src/web/app/desktop/tags/pages/home-customize.tag new file mode 100644 index 0000000000..4434015615 --- /dev/null +++ b/src/web/app/desktop/tags/pages/home-customize.tag @@ -0,0 +1,14 @@ + + + + + + + diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag index eabddfb432..4c16f9eaa8 100644 --- a/src/web/app/desktop/tags/settings.tag +++ b/src/web/app/desktop/tags/settings.tag @@ -38,6 +38,7 @@

デザイン

+ ホームをカスタマイズ
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag index 0a3e8d9c53..6a4982877f 100644 --- a/src/web/app/desktop/tags/ui.tag +++ b/src/web/app/desktop/tags/ui.tag @@ -37,7 +37,7 @@ - +
diff --git a/src/web/app/init.js b/src/web/app/init.js index 5a6899ed4f..7e3c2ee377 100644 --- a/src/web/app/init.js +++ b/src/web/app/init.js @@ -11,7 +11,6 @@ import checkForUpdate from './common/scripts/check-for-update'; import Connection from './common/scripts/home-stream'; import Progress from './common/scripts/loading'; import mixin from './common/mixins'; -import generateDefaultUserdata from './common/scripts/generate-default-userdata'; import CONFIG from './common/scripts/config'; require('./common/tags'); @@ -156,9 +155,7 @@ function fetchme(token, cb) { res.json().then(i => { me = i; me.token = token; - - // initialize it if user data is empty - me.data ? done() : init(); + done(); }); }, () => { // When failure // Render the error screen @@ -170,17 +167,6 @@ function fetchme(token, cb) { function done() { if (cb) cb(me); } - - // Initialize user data - function init() { - const data = generateDefaultUserdata(); - api(token, 'i/appdata/set', { - data - }).then(() => { - me.data = data; - done(); - }); - } } // BSoD diff --git a/tools/migration/node.2017-11-08..js b/tools/migration/node.2017-11-08..js new file mode 100644 index 0000000000..e25b83b3f3 --- /dev/null +++ b/tools/migration/node.2017-11-08..js @@ -0,0 +1,89 @@ +const uuid = require('uuid'); +const { default: User } = require('../../built/api/models/user') +const { default: zip } = require('@prezzemolo/zip') + +const home = { + left: [ + 'profile', + 'calendar', + 'activity', + 'rss-reader', + 'trends', + 'photo-stream', + 'version' + ], + right: [ + 'broadcast', + 'notifications', + 'user-recommendation', + 'recommended-polls', + 'server', + 'donation', + 'nav', + 'tips' + ] +}; + + +const migrate = async (doc) => { + + //#region Construct home data + const homeData = []; + + home.left.forEach(widget => { + homeData.push({ + name: widget, + id: uuid(), + place: 'left', + data: {} + }); + }); + + home.right.forEach(widget => { + homeData.push({ + name: widget, + id: uuid(), + place: 'right', + data: {} + }); + }); + //#endregion + + const result = await User.update(doc._id, { + $unset: { + data: '' + }, + $set: { + 'settings': {}, + 'client_settings.home': homeData, + 'client_settings.show_donation': false + } + }) + + return added && result.ok === 1 +} + +async function main() { + const count = await db.get('users').count(); + + console.log(`there are ${count} users.`) + + const dop = Number.parseInt(process.argv[2]) || 5 + const idop = ((count - (count % dop)) / dop) + 1 + + return zip( + 1, + async (time) => { + console.log(`${time} / ${idop}`) + const docs = await db.get('users').find({}, { limit: dop, skip: time * dop }) + return Promise.all(docs.map(migrate)) + }, + idop + ).then(a => { + const rv = [] + a.forEach(e => rv.push(...e)) + return rv + }) +} + +main().then(console.dir).catch(console.error)