diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2016-12-29 07:49:51 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2016-12-29 07:49:51 +0900 |
| commit | b3f42e62af698a67c2250533c437569559f1fdf9 (patch) | |
| tree | cdf6937576e99cccf85e6fa3aa8860a1173c7cfb /src/web/app/mobile | |
| download | sharkey-b3f42e62af698a67c2250533c437569559f1fdf9.tar.gz sharkey-b3f42e62af698a67c2250533c437569559f1fdf9.tar.bz2 sharkey-b3f42e62af698a67c2250533c437569559f1fdf9.zip | |
Initial commit :four_leaf_clover:
Diffstat (limited to 'src/web/app/mobile')
52 files changed, 4851 insertions, 0 deletions
diff --git a/src/web/app/mobile/mixins.ls b/src/web/app/mobile/mixins.ls new file mode 100644 index 0000000000..902774f91a --- /dev/null +++ b/src/web/app/mobile/mixins.ls @@ -0,0 +1,19 @@ +riot = require \riot + +module.exports = (me) ~> + if me? + (require './scripts/stream.ls') me + + require './scripts/ui.ls' + + riot.mixin \open-post-form do + open-post-form: (opts) -> + app = document.get-element-by-id \app + app.style.display = \none + form = document.body.append-child document.create-element \mk-post-form + form = riot.mount form, opts .0 + form.on \cancel recover + form.on \post recover + + function recover + app.style.display = \block diff --git a/src/web/app/mobile/router.ls b/src/web/app/mobile/router.ls new file mode 100644 index 0000000000..33ae3e82da --- /dev/null +++ b/src/web/app/mobile/router.ls @@ -0,0 +1,110 @@ +# Router +#================================ + +riot = require \riot +route = require \page +page = null + +module.exports = (me) ~> + + # Routing + #-------------------------------- + + route \/ index + route \/i/notifications notifications + route \/i/drive drive + route \/i/drive/folder/:folder drive + route \/i/drive/file/:file drive + route \/post/new new-post + route \/post::post post + route \/search::query search + route \/:user user.bind null \posts + route \/:user/graphs user.bind null \graphs + route \/:user/followers user-followers + route \/:user/following user-following + route \/:user/:post post + route \* not-found + + # Handlers + #-------------------------------- + + # / + function index + if me? then home! else entrance! + + # ホーム + function home + mount document.create-element \mk-home-page + + # 玄関 + function entrance + mount document.create-element \mk-entrance + + # 通知 + function notifications + mount document.create-element \mk-notifications-page + + # 新規投稿 + function new-post + mount document.create-element \mk-new-post-page + + # 検索 + function search ctx + document.create-element \mk-search-page + ..set-attribute \query ctx.params.query + .. |> mount + + # ユーザー + function user page, ctx + document.create-element \mk-user-page + ..set-attribute \user ctx.params.user + ..set-attribute \page page + .. |> mount + + # フォロー一覧 + function user-following ctx + document.create-element \mk-user-following-page + ..set-attribute \user ctx.params.user + .. |> mount + + # フォロワー一覧 + function user-followers ctx + document.create-element \mk-user-followers-page + ..set-attribute \user ctx.params.user + .. |> mount + + # 投稿詳細ページ + function post ctx + document.create-element \mk-post-page + ..set-attribute \post ctx.params.post + .. |> mount + + # ドライブ + function drive ctx + p = document.create-element \mk-drive-page + if ctx.params.folder then p.set-attribute \folder ctx.params.folder + if ctx.params.file then p.set-attribute \file ctx.params.file + mount p + + # not found + function not-found + mount document.create-element \mk-not-found + + # Register mixin + #-------------------------------- + + riot.mixin \page do + page: route + + # Exec + #-------------------------------- + + route! + +# Mount +#================================ + +function mount content + if page? then page.unmount! + body = document.get-element-by-id \app + page := riot.mount body.append-child content .0 diff --git a/src/web/app/mobile/script.js b/src/web/app/mobile/script.js new file mode 100644 index 0000000000..1c269a57d9 --- /dev/null +++ b/src/web/app/mobile/script.js @@ -0,0 +1,20 @@ +/** + * Mobile Client + */ + +require('./tags.ls'); +require('./scripts/sp-slidemenu.js'); +const boot = require('../boot.ls'); +const mixins = require('./mixins.ls'); +const route = require('./router.ls'); + +/** + * Boot + */ +boot(me => { + // Register mixins + mixins(me); + + // Start routing + route(me); +}); diff --git a/src/web/app/mobile/scripts/sp-slidemenu.js b/src/web/app/mobile/scripts/sp-slidemenu.js new file mode 100644 index 0000000000..f2dcae9cef --- /dev/null +++ b/src/web/app/mobile/scripts/sp-slidemenu.js @@ -0,0 +1,839 @@ +/** + * sp-slidemenu.js + * + * @version 0.1.0 + * @url https://github.com/be-hase/sp-slidemenu + * + * Copyright 2013 be-hase.com, Inc. + * Licensed under the MIT License: + * http://www.opensource.org/licenses/mit-license.php + */ + +/** + * CUSTOMIZED BY SYUILO + */ + +; (function(window, document, undefined) { + "use strict"; + var div, PREFIX, support, gestureStart, EVENTS, ANIME_SPEED, SLIDE_STATUS, SCROLL_STATUS, THRESHOLD, EVENT_MOE_TIME, rclass, ITEM_CLICK_CLASS_NAME; + div = document.createElement('div'); + PREFIX = ['webkit', 'moz', 'o', 'ms']; + support = SpSlidemenu.support = {}; + support.transform3d = hasProp([ + 'perspectiveProperty', + 'WebkitPerspective', + 'MozPerspective', + 'OPerspective', + 'msPerspective' + ]); + support.transform = hasProp([ + 'transformProperty', + 'WebkitTransform', + 'MozTransform', + 'OTransform', + 'msTransform' + ]); + support.transition = hasProp([ + 'transitionProperty', + 'WebkitTransitionProperty', + 'MozTransitionProperty', + 'OTransitionProperty', + 'msTransitionProperty' + ]); + support.addEventListener = 'addEventListener' in window; + support.msPointer = window.navigator.msPointerEnabled; + support.cssAnimation = (support.transform3d || support.transform) && support.transition; + support.touch = 'ontouchend' in window; + EVENTS = { + start: { + touch: 'touchstart', + mouse: 'mousedown' + }, + move: { + touch: 'touchmove', + mouse: 'mousemove' + }, + end: { + touch: 'touchend', + mouse: 'mouseup' + } + }; + gestureStart = false; + if (support.addEventListener) { + document.addEventListener('gesturestart', function() { + gestureStart = true; + }); + document.addEventListener('gestureend', function() { + gestureStart = false; + }); + } + ANIME_SPEED = { + slider: 200, + scrollOverBack: 400 + }; + SLIDE_STATUS = { + close: 0, + open: 1, + progress: 2 + }; + THRESHOLD = 10; + EVENT_MOE_TIME = 50; + rclass = /[\t\r\n\f]/g; + ITEM_CLICK_CLASS_NAME = 'menu-item'; + /* + [MEMO] + SpSlidemenu properties which is not function is ... + -- element -- + element: main + element: slidemenu + element: button + element: slidemenuBody + element: slidemenuContent + element: slidemenuHeader + -- options -- + bool: disableCssAnimation + bool: disabled3d + -- animation -- + bool: useCssAnimation + bool: use3d + -- slide -- + int: slideWidth + string: htmlOverflowX + string: bodyOverflowX + int: buttonStartPageX + int: buttonStartPageY + -- scroll -- + bool: scrollTouchStarted + bool: scrollMoveReady + int: scrollStartPageX + int: scrollStartPageY + int: scrollBasePageY + int: scrollTimeForVelocity + int: scrollCurrentY + int: scrollMoveEventCnt + int: scrollAnimationTimer + int: scrollOverTimer + int: scrollMaxY + */ + function SpSlidemenu(main, slidemenu, button, options) { + if (this instanceof SpSlidemenu) { + return this.init(main, slidemenu, button, options); + } else { + return new SpSlidemenu(main, slidemenu, button, options); + } + } + SpSlidemenu.prototype.init = function(main, slidemenu, button, options) { + var _this = this; + // find and set element. + _this.setElement(main, slidemenu, button); + if (!_this.main || !_this.slidemenu || !_this.button || !_this.slidemenuBody || !_this.slidemenuContent) { + throw new Error('Element not found. Please set correctly.'); + } + // options + options = options || {}; + _this.disableCssAnimation = (options.disableCssAnimation === undefined) ? false : options.disableCssAnimation; + _this.disable3d = (options.disable3d === undefined) ? false : options.disable3d; + _this.direction = 'left'; + if (options.direction === 'right') { + _this.direction = 'right'; + } + // animation + _this.useCssAnimation = support.cssAnimation; + if (_this.disableCssAnimation === true) { + _this.useCssAnimation = false; + } + _this.use3d = support.transform3d; + if (_this.disable3d === true) { + _this.use3d = false; + } + // slide + _this.slideWidth = (getDimentions(_this.slidemenu)).width; + _this.main.SpSlidemenuStatus = SLIDE_STATUS.close; + _this.htmlOverflowX = ''; + _this.bodyOverflowX = ''; + // scroll + _this.scrollCurrentY = 0; + _this.scrollAnimationTimer = false; + _this.scrollOverTimer = false; + // set default style. + _this.setDefaultStyle(); + // bind some method for callback. + _this.bindMethods(); + // add event + addTouchEvent('start', _this.button, _this.buttonTouchStart, false); + addTouchEvent('move', _this.button, blockEvent, false); + addTouchEvent('end', _this.button, _this.buttonTouchEnd, false); + addTouchEvent('start', _this.slidemenuContent, _this.scrollTouchStart, false); + addTouchEvent('move', _this.slidemenuContent, _this.scrollTouchMove, false); + addTouchEvent('end', _this.slidemenuContent, _this.scrollTouchEnd, false); + _this.slidemenuContent.addEventListener('click', _this.itemClick, false); + // window size change + window.addEventListener('resize', debounce(_this.setHeight, 100), false); + return _this; + }; + SpSlidemenu.prototype.bindMethods = function() { + var _this, funcs; + _this = this; + funcs = [ + 'setHeight', + 'slideOpen', 'slideOpenEnd', 'slideClose', 'slideCloseEnd', + 'buttonTouchStart', 'buttonTouchEnd', 'mainTouchStart', + 'scrollTouchStart', 'scrollTouchMove', 'scrollTouchEnd', 'scrollInertiaMove', 'scrollOverBack', 'scrollOver', + 'itemClick' + ]; + funcs.forEach(function(func) { + _this[func] = bind(_this[func], _this); + }); + }; + SpSlidemenu.prototype.setElement = function(main, slidemenu, button) { + var _this = this; + _this.main = main; + if (typeof main === 'string') { + _this.main = document.querySelector(main); + } + _this.slidemenu = slidemenu; + if (typeof slidemenu === 'string') { + _this.slidemenu = document.querySelector(slidemenu); + } + _this.button = button; + if (typeof button === 'string') { + _this.button = document.querySelector(button); + } + if (!_this.slidemenu) { + return; + } + _this.slidemenuBody = _this.slidemenu.querySelector('.body'); + _this.slidemenuContent = _this.slidemenu.querySelector('.content'); + _this.slidemenuHeader = _this.slidemenu.querySelector('.header'); + }; + SpSlidemenu.prototype.setDefaultStyle = function() { + var _this = this; + if (support.msPointer) { + _this.slidemenuContent.style.msTouchAction = 'none'; + } + _this.setHeight(); + if (_this.useCssAnimation) { + setStyles(_this.main, { + transitionProperty: getCSSName('transform'), + transitionTimingFunction: 'ease-in-out', + transitionDuration: ANIME_SPEED.slider + 'ms', + transitionDelay: '0ms', + transform: _this.getTranslateX(0) + }); + setStyles(_this.slidemenu, { + transitionProperty: 'visibility', + transitionTimingFunction: 'linear', + transitionDuration: '0ms', + transitionDelay: ANIME_SPEED.slider + 'ms' + }); + setStyles(_this.slidemenuContent, { + transitionProperty: getCSSName('transform'), + transitionTimingFunction: 'ease-in-out', + transitionDuration: '0ms', + transitionDelay: '0ms', + transform: _this.getTranslateY(0) + }); + } else { + setStyles(_this.main, { + position: 'relative', + left: '0px' + }); + setStyles(_this.slidemenuContent, { + top: '0px' + }); + } + }; + SpSlidemenu.prototype.setHeight = function(event) { + var _this, browserHeight; + _this = this; + browserHeight = getBrowserHeight(); + setStyles(_this.main, { + minHeight: browserHeight + 'px' + }); + setStyles(_this.slidemenu, { + height: browserHeight + 'px' + }); + }; + SpSlidemenu.prototype.buttonTouchStart = function(event) { + var _this = this; + event.preventDefault(); + event.stopPropagation(); + switch (_this.main.SpSlidemenuStatus) { + case SLIDE_STATUS.progress: + break; + case SLIDE_STATUS.open: + case SLIDE_STATUS.close: + _this.buttonStartPageX = getPage(event, 'pageX'); + _this.buttonStartPageY = getPage(event, 'pageY'); + break; + } + }; + SpSlidemenu.prototype.buttonTouchEnd = function(event) { + var _this = this; + event.preventDefault(); + event.stopPropagation(); + if (_this.shouldTrigerNext(event)) { + switch (_this.main.SpSlidemenuStatus) { + case SLIDE_STATUS.progress: + break; + case SLIDE_STATUS.open: + _this.slideClose(event); + break; + case SLIDE_STATUS.close: + _this.slideOpen(event); + break; + } + } + }; + SpSlidemenu.prototype.mainTouchStart = function(event) { + var _this = this; + event.preventDefault(); + event.stopPropagation(); + _this.slideClose(event); + }; + SpSlidemenu.prototype.shouldTrigerNext = function(event) { + var _this = this, + buttonEndPageX = getPage(event, 'pageX'), + buttonEndPageY = getPage(event, 'pageY'), + deltaX = Math.abs(buttonEndPageX - _this.buttonStartPageX), + deltaY = Math.abs(buttonEndPageY - _this.buttonStartPageY); + return deltaX < 20 && deltaY < 20; + }; + SpSlidemenu.prototype.slideOpen = function(event) { + var _this = this, toX; + + /// Misskey Original + document.body.setAttribute('data-nav-open', 'true'); + + if (_this.direction === 'left') { + toX = _this.slideWidth; + } else { + toX = -_this.slideWidth; + } + _this.main.SpSlidemenuStatus = SLIDE_STATUS.progress; + //set event + addTouchEvent('move', document, blockEvent, false); + // change style + _this.htmlOverflowX = document.documentElement.style['overflowX']; + _this.bodyOverflowX = document.body.style['overflowX']; + document.documentElement.style['overflowX'] = document.body.style['overflowX'] = 'hidden'; + if (_this.useCssAnimation) { + setStyles(_this.main, { + transform: _this.getTranslateX(toX) + }); + setStyles(_this.slidemenu, { + transitionProperty: 'z-index', + visibility: 'visible', + zIndex: '1' + }); + } else { + animate(_this.main, _this.direction, toX, ANIME_SPEED.slider); + setStyles(_this.slidemenu, { + visibility: 'visible' + }); + } + // set callback + setTimeout(_this.slideOpenEnd, ANIME_SPEED.slider + EVENT_MOE_TIME); + }; + SpSlidemenu.prototype.slideOpenEnd = function() { + var _this = this; + _this.main.SpSlidemenuStatus = SLIDE_STATUS.open; + // change style + if (_this.useCssAnimation) { + } else { + setStyles(_this.slidemenu, { + zIndex: '1' + }); + } + // add event + addTouchEvent('start', _this.main, _this.mainTouchStart, false); + }; + SpSlidemenu.prototype.slideClose = function(event) { + var _this = this; + _this.main.SpSlidemenuStatus = SLIDE_STATUS.progress; + + /// Misskey Original + document.body.setAttribute('data-nav-open', 'false'); + + //event + removeTouchEvent('start', _this.main, _this.mainTouchStart, false); + // change style + if (_this.useCssAnimation) { + setStyles(_this.main, { + transform: _this.getTranslateX(0) + }); + setStyles(_this.slidemenu, { + transitionProperty: 'visibility', + visibility: 'hidden', + zIndex: '-1' + }); + } else { + animate(_this.main, _this.direction, 0, ANIME_SPEED.slider); + setStyles(_this.slidemenu, { + zIndex: '-1' + }); + } + // set callback + setTimeout(_this.slideCloseEnd, ANIME_SPEED.slider + EVENT_MOE_TIME); + }; + SpSlidemenu.prototype.slideCloseEnd = function() { + var _this = this; + _this.main.SpSlidemenuStatus = SLIDE_STATUS.close; + // change style + document.documentElement.style['overflowX'] = _this.htmlOverflowX; + document.body.style['overflowX'] = _this.bodyOverflowX; + if (_this.useCssAnimation) { + } else { + setStyles(_this.slidemenu, { + visibility: 'hidden' + }); + } + // set event + removeTouchEvent('move', document, blockEvent, false); + }; + SpSlidemenu.prototype.scrollTouchStart = function(event) { + var _this = this; + if (gestureStart) { + return; + } + if (_this.scrollOverTimer !== false) { + clearTimeout(_this.scrollOverTimer); + } + _this.scrollCurrentY = _this.getScrollCurrentY(); + if (_this.useCssAnimation) { + setStyles(_this.slidemenuContent, { + transitionTimingFunction: 'ease-in-out', + transitionDuration: '0ms', + transform: _this.getTranslateY(_this.scrollCurrentY) + }); + } else { + _this.stopScrollAnimate(); + setStyles(_this.slidemenuContent, { + top: _this.scrollCurrentY + 'px' + }); + } + _this.scrollOverTimer = false; + _this.scrollAnimationTimer = false; + _this.scrollTouchStarted = true; + _this.scrollMoveReady = false; + _this.scrollMoveEventCnt = 0; + _this.scrollMaxY = _this.calcMaxY(); + _this.scrollStartPageX = getPage(event, 'pageX'); + _this.scrollStartPageY = getPage(event, 'pageY'); + _this.scrollBasePageY = _this.scrollStartPageY; + _this.scrollTimeForVelocity = event.timeStamp; + _this.scrollPageYForVelocity = _this.scrollStartPageY; + _this.slidemenuContent.removeEventListener('click', blockEvent, true); + }; + SpSlidemenu.prototype.scrollTouchMove = function(event) { + var _this, pageX, pageY, distY, newY, deltaX, deltaY; + _this = this; + if (!_this.scrollTouchStarted || gestureStart) { + return; + } + pageX = getPage(event, 'pageX'); + pageY = getPage(event, 'pageY'); + if (_this.scrollMoveReady) { + event.preventDefault(); + event.stopPropagation(); + distY = pageY - _this.scrollBasePageY; + newY = _this.scrollCurrentY + distY; + if (newY > 0 || newY < _this.scrollMaxY) { + newY = Math.round(_this.scrollCurrentY + distY / 3); + } + _this.scrollSetY(newY); + if (_this.scrollMoveEventCnt % THRESHOLD === 0) { + _this.scrollPageYForVelocity = pageY; + _this.scrollTimeForVelocity = event.timeStamp; + } + _this.scrollMoveEventCnt++; + } else { + deltaX = Math.abs(pageX - _this.scrollStartPageX); + deltaY = Math.abs(pageY - _this.scrollStartPageY); + if (deltaX > 5 || deltaY > 5) { + _this.scrollMoveReady = true; + _this.slidemenuContent.addEventListener('click', blockEvent, true); + } + } + _this.scrollBasePageY = pageY; + }; + SpSlidemenu.prototype.scrollTouchEnd = function(event) { + var _this, speed, deltaY, deltaTime; + _this = this; + if (!_this.scrollTouchStarted) { + return; + } + _this.scrollTouchStarted = false; + _this.scrollMaxY = _this.calcMaxY(); + if (_this.scrollCurrentY > 0 || _this.scrollCurrentY < _this.scrollMaxY) { + _this.scrollOverBack(); + return; + } + deltaY = getPage(event, 'pageY') - _this.scrollPageYForVelocity; + deltaTime = event.timeStamp - _this.scrollTimeForVelocity; + speed = deltaY / deltaTime; + if (Math.abs(speed) >= 0.01) { + _this.scrollInertia(speed); + } + }; + SpSlidemenu.prototype.scrollInertia = function(speed) { + var _this, directionToTop, maxTo, distanceMaxTo, stopTime, canMove, to, duration, speedAtboundary, nextTo; + _this = this; + if (speed > 0) { + directionToTop = true; + maxTo = 0; + } else { + directionToTop = false; + maxTo = _this.scrollMaxY; + } + distanceMaxTo = Math.abs(_this.scrollCurrentY - maxTo); + speed = Math.abs(750 * speed); + if (speed > 1000) { + speed = 1000; + } + stopTime = speed / 500; + canMove = (speed * stopTime) - ((500 * Math.pow(stopTime, 2)) / 2); + if (canMove <= distanceMaxTo) { + if (directionToTop) { + to = _this.scrollCurrentY + canMove; + } else { + to = _this.scrollCurrentY - canMove; + } + duration = stopTime * 1000; + _this.scrollInertiaMove(to, duration, false); + } else { + to = maxTo; + speedAtboundary = Math.sqrt((2 * 500 * distanceMaxTo) + Math.pow(speed, 2)); + duration = (speedAtboundary - speed) / 500 * 1000; + _this.scrollInertiaMove(to, duration, true, speedAtboundary, directionToTop); + } + }; + SpSlidemenu.prototype.scrollInertiaMove = function(to, duration, isOver, speed, directionToTop) { + var _this = this, stopTime, canMove; + _this.scrollCurrentY = to; + if (_this.useCssAnimation) { + setStyles(_this.slidemenuContent, { + transitionTimingFunction: 'cubic-bezier(0.33, 0.66, 0.66, 1)', + transitionDuration: duration + 'ms', + transform: _this.getTranslateY(to) + }); + } else { + _this.scrollAnimate(to, duration); + } + if (!isOver) { + return; + } + stopTime = speed / 7500; + canMove = (speed * stopTime) - ((7500 * Math.pow(stopTime, 2)) / 2); + if (directionToTop) { + to = _this.scrollCurrentY + canMove; + } else { + to = _this.scrollCurrentY - canMove; + } + duration = stopTime * 1000; + _this.scrollOver(to, duration); + }; + SpSlidemenu.prototype.scrollOver = function(to, duration) { + var _this; + _this = this; + _this.scrollCurrentY = to; + if (_this.useCssAnimation) { + setStyles(_this.slidemenuContent, { + transitionTimingFunction: 'cubic-bezier(0.33, 0.66, 0.66, 1)', + transitionDuration: duration + 'ms', + transform: _this.getTranslateY(to) + }); + } else { + _this.scrollAnimate(to, duration); + } + _this.scrollOverTimer = setTimeout(_this.scrollOverBack, duration); + }; + SpSlidemenu.prototype.scrollOverBack = function() { + var _this, to; + _this = this; + if (_this.scrollCurrentY >= 0) { + to = 0; + } else { + to = _this.scrollMaxY; + } + _this.scrollCurrentY = to; + if (_this.useCssAnimation) { + setStyles(_this.slidemenuContent, { + transitionTimingFunction: 'ease-out', + transitionDuration: ANIME_SPEED.scrollOverBack + 'ms', + transform: _this.getTranslateY(to) + }); + } else { + _this.scrollAnimate(to, ANIME_SPEED.scrollOverBack); + } + }; + SpSlidemenu.prototype.scrollSetY = function(y) { + var _this = this; + _this.scrollCurrentY = y; + if (_this.useCssAnimation) { + setStyles(_this.slidemenuContent, { + transitionTimingFunction: 'ease-in-out', + transitionDuration: '0ms', + transform: _this.getTranslateY(y) + }); + } else { + _this.slidemenuContent.style.top = y + 'px'; + } + }; + SpSlidemenu.prototype.scrollAnimate = function(to, transitionDuration) { + var _this = this; + _this.stopScrollAnimate(); + _this.scrollAnimationTimer = animate(_this.slidemenuContent, 'top', to, transitionDuration); + }; + SpSlidemenu.prototype.stopScrollAnimate = function() { + var _this = this; + if (_this.scrollAnimationTimer !== false) { + clearInterval(_this.scrollAnimationTimer); + } + }; + SpSlidemenu.prototype.itemClick = function(event) { + var elem = event.target || event.srcElement; + if (hasClass(elem, ITEM_CLICK_CLASS_NAME)) { + this.slideClose(); + } + }; + SpSlidemenu.prototype.calcMaxY = function(x) { + var _this, contentHeight, bodyHeight, headerHeight; + _this = this; + contentHeight = _this.slidemenuContent.offsetHeight; + bodyHeight = _this.slidemenuBody.offsetHeight; + headerHeight = 0; + if (_this.slidemenuHeader) { + headerHeight = _this.slidemenuHeader.offsetHeight; + } + if (contentHeight > bodyHeight) { + return -(contentHeight - bodyHeight + headerHeight); + } else { + return 0; + } + }; + SpSlidemenu.prototype.getScrollCurrentY = function() { + var ret = 0; + if (this.useCssAnimation) { + getStyle(window.getComputedStyle(this.slidemenuContent, ''), 'transform').split(',').forEach(function(value) { + var number = parseInt(value, 10); + if (!isNaN(number) && number !== 0 && number !== 1) { + ret = number; + } + }); + } else { + var number = parseInt(getStyle(window.getComputedStyle(this.slidemenuContent, ''), 'top'), 10); + if (!isNaN(number) && number !== 0 && number !== 1) { + ret = number; + } + } + return ret; + }; + SpSlidemenu.prototype.getTranslateX = function(x) { + var _this = this; + return _this.use3d ? 'translate3d(' + x + 'px, 0px, 0px)' : 'translate(' + x + 'px, 0px)'; + }; + SpSlidemenu.prototype.getTranslateY = function(y) { + var _this = this; + return _this.use3d ? 'translate3d(0px, ' + y + 'px, 0px)' : 'translate(0px, ' + y + 'px)'; + }; + //Utility Function + function hasProp(props) { + return some(props, function(prop) { + return div.style[prop] !== undefined; + }); + } + function upperCaseFirst(str) { + return str.charAt(0).toUpperCase() + str.substr(1); + } + function some(ary, callback) { + var i, len; + for (i = 0, len = ary.length; i < len; i++) { + if (callback(ary[i], i)) { + return true; + } + } + return false; + } + function setStyle(elem, prop, val) { + var style = elem.style; + if (!setStyle.cache) { + setStyle.cache = {}; + } + if (setStyle.cache[prop] !== undefined) { + style[setStyle.cache[prop]] = val; + return; + } + if (style[prop] !== undefined) { + setStyle.cache[prop] = prop; + style[prop] = val; + return; + } + some(PREFIX, function(_prefix) { + var _prop = upperCaseFirst(_prefix) + upperCaseFirst(prop); + if (style[_prop] !== undefined) { + //setStyle.cache[prop] = _prop; + style[_prop] = val; + return true; + } + }); + } + function setStyles(elem, styles) { + var style, prop; + for (prop in styles) { + if (styles.hasOwnProperty(prop)) { + setStyle(elem, prop, styles[prop]); + } + } + } + function getStyle(style, prop) { + var ret; + if (style[prop] !== undefined) { + return style[prop]; + } + some(PREFIX, function(_prefix) { + var _prop = upperCaseFirst(_prefix) + upperCaseFirst(prop); + if (style[_prop] !== undefined) { + ret = style[_prop]; + return true; + } + }); + return ret; + } + function getCSSName(prop) { + var ret; + if (!getCSSName.cache) { + getCSSName.cache = {}; + } + if (getCSSName.cache[prop] !== undefined) { + return getCSSName.cache[prop]; + } + if (div.style[prop] !== undefined) { + getCSSName.cache[prop] = prop; + return prop; + } + some(PREFIX, function(_prefix) { + var _prop = upperCaseFirst(_prefix) + upperCaseFirst(prop); + if (div.style[_prop] !== undefined) { + ret = '-' + _prefix + '-' + prop; + return true; + } + }); + getCSSName.cache[prop] = ret; + return ret; + } + function bind(func, context) { + var nativeBind, slice, args; + nativeBind = Function.prototype.bind; + slice = Array.prototype.slice; + if (func.bind === nativeBind && nativeBind) { + return nativeBind.apply(func, slice.call(arguments, 1)); + } + args = slice.call(arguments, 2); + return function() { + return func.apply(context, args.concat(slice.call(arguments))); + }; + } + function blockEvent(event) { + event.preventDefault(); + event.stopPropagation(); + } + function getDimentions(element) { + var previous, key, properties, result; + previous = {}; + properties = { + position: 'absolute', + visibility: 'hidden', + display: 'block' + }; + for (key in properties) { + previous[key] = element.style[key]; + element.style[key] = properties[key]; + } + result = { + width: element.offsetWidth, + height: element.offsetHeight + }; + for (key in properties) { + element.style[key] = previous[key]; + } + return result; + } + function getPage(event, page) { + return event.changedTouches ? event.changedTouches[0][page] : event[page]; + } + function addTouchEvent(eventType, element, listener, useCapture) { + useCapture = useCapture || false; + if (support.touch) { + element.addEventListener(EVENTS[eventType].touch, listener, { passive: useCapture }); + } else { + element.addEventListener(EVENTS[eventType].mouse, listener, { passive: useCapture }); + } + } + function removeTouchEvent(eventType, element, listener, useCapture) { + useCapture = useCapture || false; + if (support.touch) { + element.removeEventListener(EVENTS[eventType].touch, listener, useCapture); + } else { + element.removeEventListener(EVENTS[eventType].mouse, listener, useCapture); + } + } + function hasClass(elem, className) { + className = " " + className + " "; + if (elem.nodeType === 1 && (" " + elem.className + " ").replace(rclass, " ").indexOf(className) >= 0) { + return true; + } + return false; + } + function animate(elem, prop, to, transitionDuration) { + var begin, from, duration, easing, timer; + begin = +new Date(); + from = parseInt(elem.style[prop], 10); + to = parseInt(to, 10); + duration = parseInt(transitionDuration, 10); + easing = function(time, duration) { + return -(time /= duration) * (time - 2); + }; + timer = setInterval(function() { + var time, pos, now; + time = new Date() - begin; + if (time > duration) { + clearInterval(timer); + now = to; + } else { + pos = easing(time, duration); + now = pos * (to - from) + from; + } + elem.style[prop] = now + 'px'; + }, 10); + return timer; + } + function getBrowserHeight() { + if (window.innerHeight) { + return window.innerHeight; + } + else if (document.documentElement && document.documentElement.clientHeight !== 0) { + return document.documentElement.clientHeight; + } + else if (document.body) { + return document.body.clientHeight; + } + return 0; + } + function debounce(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) result = func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) result = func.apply(context, args); + return result; + }; + } + window.SpSlidemenu = SpSlidemenu; +})(window, window.document); diff --git a/src/web/app/mobile/scripts/stream.ls b/src/web/app/mobile/scripts/stream.ls new file mode 100644 index 0000000000..b7810b49ae --- /dev/null +++ b/src/web/app/mobile/scripts/stream.ls @@ -0,0 +1,13 @@ +# Stream +#================================ + +stream = require '../../common/scripts/stream.ls' +riot = require \riot + +module.exports = (me) ~> + s = stream me + + riot.mixin \stream do + stream: s.event + get-stream-state: s.get-state + stream-state-ev: s.state-ev diff --git a/src/web/app/mobile/scripts/ui.ls b/src/web/app/mobile/scripts/ui.ls new file mode 100644 index 0000000000..aa94a8b052 --- /dev/null +++ b/src/web/app/mobile/scripts/ui.ls @@ -0,0 +1,6 @@ +riot = require \riot + +ui = riot.observable! + +riot.mixin \ui do + ui: ui diff --git a/src/web/app/mobile/style.styl b/src/web/app/mobile/style.styl new file mode 100644 index 0000000000..bc7859844a --- /dev/null +++ b/src/web/app/mobile/style.styl @@ -0,0 +1,12 @@ +@import "../base" + +body[data-nav-open='true'] + #hamburger + > i + -webkit-transform rotate(-90deg) + transform rotate(-90deg) + +#wait + top auto + bottom 15px + left 15px diff --git a/src/web/app/mobile/tags.ls b/src/web/app/mobile/tags.ls new file mode 100644 index 0000000000..5805cb6b28 --- /dev/null +++ b/src/web/app/mobile/tags.ls @@ -0,0 +1,44 @@ +require './tags/ui.tag' +require './tags/ui-header.tag' +require './tags/ui-nav.tag' +require './tags/stream-indicator.tag' +require './tags/page/entrance.tag' +require './tags/page/entrance/signin.tag' +require './tags/page/entrance/signup.tag' +require './tags/page/home.tag' +require './tags/page/drive.tag' +require './tags/page/notifications.tag' +require './tags/page/user.tag' +require './tags/page/user-followers.tag' +require './tags/page/user-following.tag' +require './tags/page/post.tag' +require './tags/page/new-post.tag' +require './tags/page/search.tag' +require './tags/home.tag' +require './tags/home-timeline.tag' +require './tags/timeline.tag' +require './tags/timeline-post.tag' +require './tags/timeline-post-sub.tag' +require './tags/post-preview.tag' +require './tags/sub-post-content.tag' +require './tags/images-viewer.tag' +require './tags/drive.tag' +require './tags/drive-selector.tag' +require './tags/drive/file.tag' +require './tags/drive/folder.tag' +require './tags/drive/file-viewer.tag' +require './tags/post-form.tag' +require './tags/notification.tag' +require './tags/notifications.tag' +require './tags/notify.tag' +require './tags/notification-preview.tag' +require './tags/search.tag' +require './tags/search-posts.tag' +require './tags/post-detail.tag' +require './tags/user.tag' +require './tags/user-timeline.tag' +require './tags/follow-button.tag' +require './tags/user-preview.tag' +require './tags/users-list.tag' +require './tags/user-following.tag' +require './tags/user-followers.tag' diff --git a/src/web/app/mobile/tags/drive-selector.tag b/src/web/app/mobile/tags/drive-selector.tag new file mode 100644 index 0000000000..442299026e --- /dev/null +++ b/src/web/app/mobile/tags/drive-selector.tag @@ -0,0 +1,75 @@ +mk-drive-selector + div.body + header + h1 + | ファイルを選択 + span.count(if={ files.length > 0 }) ({ files.length }) + button.close(onclick={ cancel }): i.fa.fa-times + button.ok(onclick={ ok }): i.fa.fa-check + mk-drive@browser(select={ true }, multiple={ opts.multiple }) + +style. + display block + + > .body + position fixed + z-index 2048 + top 0 + left 0 + right 0 + margin 0 auto + width 100% + max-width 500px + height 100% + overflow hidden + background #fff + box-shadow 0 0 16px rgba(#000, 0.3) + + > header + border-bottom solid 1px #eee + + > h1 + margin 0 + padding 0 + text-align center + line-height 42px + font-size 1em + font-weight normal + + > .count + margin-left 4px + opacity 0.5 + + > .close + position absolute + top 0 + left 0 + line-height 42px + width 42px + + > .ok + position absolute + top 0 + right 0 + line-height 42px + width 42px + + > mk-drive + height calc(100% - 42px) + overflow scroll + +script. + @files = [] + + @on \mount ~> + @refs.browser.on \change-selected (files) ~> + @files = files + @update! + + @cancel = ~> + @trigger \canceled + @unmount! + + @ok = ~> + @trigger \selected @files + @unmount! diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag new file mode 100644 index 0000000000..fcc78d1e68 --- /dev/null +++ b/src/web/app/mobile/tags/drive.tag @@ -0,0 +1,338 @@ +mk-drive + nav + p(onclick={ go-root }) + i.fa.fa-cloud + | ドライブ + virtual(each={ folder in hierarchy-folders }) + span: i.fa.fa-angle-right + p(onclick={ _move }) { folder.name } + span(if={ folder != null }): i.fa.fa-angle-right + p(if={ folder != null }) { folder.name } + div.browser(if={ file == null }, class={ loading: loading }) + div.folders(if={ folders.length > 0 }) + virtual(each={ folder in folders }) + mk-drive-folder(folder={ folder }) + p(if={ more-folders }) + | もっと読み込む + div.files(if={ files.length > 0 }) + virtual(each={ file in files }) + mk-drive-file(file={ file }) + p(if={ more-files }) + | もっと読み込む + div.empty(if={ files.length == 0 && folders.length == 0 && !loading }) + p(if={ !folder == null }) + | ドライブには何もありません。 + p(if={ folder != null }) + | このフォルダーは空です + div.loading(if={ loading }). + <div class="spinner"> + <div class="dot1"></div> + <div class="dot2"></div> + </div> + mk-drive-file-viewer(if={ file != null }, file={ file }) + +style. + display block + background #fff + + > nav + display block + width 100% + padding 10px 12px + overflow auto + white-space nowrap + font-size 0.9em + color #555 + background #fff + border-bottom solid 1px #dfdfdf + + > p + display inline + margin 0 + padding 0 + + &:last-child + font-weight bold + + > i + margin-right 4px + + > span + margin 0 8px + opacity 0.5 + + > .browser + &.loading + opacity 0.5 + + > .folders + > mk-drive-folder + border-bottom solid 1px #eee + + > .files + > mk-drive-file + border-bottom solid 1px #eee + + > .empty + padding 16px + text-align center + color #999 + pointer-events none + + > p + margin 0 + + > .loading + .spinner + margin 100px auto + width 40px + height 40px + text-align center + + animation sk-rotate 2.0s infinite linear + + .dot1, .dot2 + width 60% + height 60% + display inline-block + position absolute + top 0 + background-color rgba(0, 0, 0, 0.3) + border-radius 100% + + animation sk-bounce 2.0s infinite ease-in-out + + .dot2 + top auto + bottom 0 + animation-delay -1.0s + + @keyframes sk-rotate { 100% { transform: rotate(360deg); }} + + @keyframes sk-bounce { + 0%, 100% { + transform: scale(0.0); + } 50% { + transform: scale(1.0); + } + } + +script. + @mixin \api + @mixin \stream + + @files = [] + @folders = [] + @hierarchy-folders = [] + @selected-files = [] + + # 現在の階層(フォルダ) + # * null でルートを表す + @folder = null + + @file = null + + @is-select-mode = @opts.select? and @opts.select + @multiple = if @opts.multiple? then @opts.multiple else false + + @on \mount ~> + @stream.on \drive_file_created @on-stream-drive-file-created + @stream.on \drive_file_updated @on-stream-drive-file-updated + @stream.on \drive_folder_created @on-stream-drive-folder-created + @stream.on \drive_folder_updated @on-stream-drive-folder-updated + + # Riotのバグでnullを渡しても""になる + # https://github.com/riot/riot/issues/2080 + #if @opts.folder? + if @opts.folder? and @opts.folder != '' + @cd @opts.folder + else + @load! + + @on \unmount ~> + @stream.off \drive_file_created @on-stream-drive-file-created + @stream.off \drive_file_updated @on-stream-drive-file-updated + @stream.off \drive_folder_created @on-stream-drive-folder-created + @stream.off \drive_folder_updated @on-stream-drive-folder-updated + + @on-stream-drive-file-created = (file) ~> + @add-file file, true + + @on-stream-drive-file-updated = (file) ~> + current = if @folder? then @folder.id else null + if current != file.folder_id + @remove-file file + else + @add-file file, true + + @on-stream-drive-folder-created = (folder) ~> + @add-folder folder, true + + @on-stream-drive-folder-updated = (folder) ~> + current = if @folder? then @folder.id else null + if current != folder.parent_id + @remove-folder folder + else + @add-folder folder, true + + @_move = (ev) ~> + @move ev.item.folder + + @move = (target-folder) ~> + @cd target-folder, true + + @cd = (target-folder, is-move) ~> + if target-folder? and typeof target-folder == \object + target-folder = target-folder.id + + if target-folder == null + @go-root! + return + + @loading = true + @update! + + @api \drive/folders/show do + folder_id: target-folder + .then (folder) ~> + @folder = folder + @hierarchy-folders = [] + + x = (f) ~> + @hierarchy-folders.unshift f + if f.parent? + x f.parent + + if folder.parent? + x folder.parent + + @update! + if is-move then @trigger \move @folder + @trigger \cd @folder + @load! + .catch (err, text-status) -> + console.error err + + @add-folder = (folder, unshift = false) ~> + current = if @folder? then @folder.id else null + if current != folder.parent_id + return + + if (@folders.some (f) ~> f.id == folder.id) + return + + if unshift + @folders.unshift folder + else + @folders.push folder + + @update! + + @add-file = (file, unshift = false) ~> + current = if @folder? then @folder.id else null + if current != file.folder_id + return + + if (@files.some (f) ~> f.id == file.id) + exist = (@files.map (f) -> f.id).index-of file.id + @files[exist] = file + @update! + return + + if unshift + @files.unshift file + else + @files.push file + + @update! + + @remove-folder = (folder) ~> + if typeof folder == \object + folder = folder.id + @folders = @folders.filter (f) -> f.id != folder + @update! + + @remove-file = (file) ~> + if typeof file == \object + file = file.id + @files = @files.filter (f) -> f.id != file + @update! + + @go-root = ~> + if @folder != null + @folder = null + @hierarchy-folders = [] + @update! + @trigger \move-root + @load! + + @load = ~> + @folders = [] + @files = [] + @more-folders = false + @more-files = false + @loading = true + @update! + + @trigger \begin-load + + load-folders = null + load-files = null + + folders-max = 20 + files-max = 20 + + # フォルダ一覧取得 + @api \drive/folders do + folder_id: if @folder? then @folder.id else null + limit: folders-max + 1 + .then (folders) ~> + if folders.length == folders-max + 1 + @more-folders = true + folders.pop! + load-folders := folders + complete! + .catch (err, text-status) ~> + console.error err + + # ファイル一覧取得 + @api \drive/files do + folder_id: if @folder? then @folder.id else null + limit: files-max + 1 + .then (files) ~> + if files.length == files-max + 1 + @more-files = true + files.pop! + load-files := files + complete! + .catch (err, text-status) ~> + console.error err + + flag = false + complete = ~> + if flag + load-folders.for-each (folder) ~> + @add-folder folder + load-files.for-each (file) ~> + @add-file file + @loading = false + @update! + + @trigger \loaded + else + flag := true + @trigger \load-mid + + @choose-file = (file) ~> + if @is-select-mode + exist = @selected-files.some (f) ~> f.id == file.id + if exist + @selected-files = (@selected-files.filter (f) ~> f.id != file.id) + else + @selected-files.push file + @update! + @trigger \change-selected @selected-files + else + @file = file + @update! + @trigger \open-file @file diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag new file mode 100644 index 0000000000..8ce89a06f4 --- /dev/null +++ b/src/web/app/mobile/tags/drive/file-viewer.tag @@ -0,0 +1,8 @@ +mk-drive-file-viewer + p.name { file.name } + +style. + display block + +script. + @file = @opts.file diff --git a/src/web/app/mobile/tags/drive/file.tag b/src/web/app/mobile/tags/drive/file.tag new file mode 100644 index 0000000000..ec271441a5 --- /dev/null +++ b/src/web/app/mobile/tags/drive/file.tag @@ -0,0 +1,130 @@ +mk-drive-file(onclick={ onclick }, data-is-selected={ is-selected }) + div.container + div.thumbnail(style={ 'background-image: url(' + file.url + '?thumbnail&size=128)' }) + div.body + p.name { file.name } + // + if file.tags.length > 0 + ul.tags + each tag in file.tags + li.tag(style={background: tag.color, color: contrast(tag.color)})= tag.name + footer + p.type + mk-file-type-icon(file={ file }) + | { file.type } + p.separator + p.data-size { bytes-to-size(file.datasize) } + p.separator + p.created-at + i.fa.fa-clock-o + mk-time(time={ file.created_at }) + +style. + display block + + &, * + user-select none + + * + pointer-events none + + > .container + max-width 500px + margin 0 auto + padding 16px + + &:after + content "" + display block + clear both + + > .thumbnail + display block + float left + width 64px + height 64px + background-size cover + background-position center center + + > .body + display block + float left + width calc(100% - 74px) + margin-left 10px + + > .name + display block + margin 0 + padding 0 + font-size 0.9em + font-weight bold + color #555 + text-overflow ellipsis + word-wrap break-word + + > .tags + display block + margin 4px 0 0 0 + padding 0 + list-style none + font-size 0.5em + + > .tag + display inline-block + margin 0 5px 0 0 + padding 1px 5px + border-radius 2px + + > footer + display block + margin 4px 0 0 0 + font-size 0.7em + + > .separator + display inline + margin 0 + padding 0 4px + color #CDCDCD + + > .type + display inline + margin 0 + padding 0 + color #9D9D9D + + > mk-file-type-icon + margin-right 4px + + > .data-size + display inline + margin 0 + padding 0 + color #9D9D9D + + > .created-at + display inline + margin 0 + padding 0 + color #BDBDBD + + > i + margin-right 2px + + &[data-is-selected] + background $theme-color + + &, * + color #fff !important + +script. + @mixin \bytes-to-size + + @browser = @parent + @file = @opts.file + @is-selected = @browser.selected-files.some (f) ~> f.id == @file.id + + @browser.on \change-selected (selects) ~> + @is-selected = selects.some (f) ~> f.id == @file.id + + @onclick = ~> + @browser.choose-file @file diff --git a/src/web/app/mobile/tags/drive/folder.tag b/src/web/app/mobile/tags/drive/folder.tag new file mode 100644 index 0000000000..ef3a72ea93 --- /dev/null +++ b/src/web/app/mobile/tags/drive/folder.tag @@ -0,0 +1,45 @@ +mk-drive-folder(onclick={ onclick }) + div.container + p.name + i.fa.fa-folder + | { folder.name } + i.fa.fa-angle-right + +style. + display block + color #777 + + &, * + user-select none + + * + pointer-events none + + > .container + max-width 500px + margin 0 auto + padding 16px + + > .name + display block + margin 0 + padding 0 + + > i + margin-right 6px + + > i + position absolute + top 0 + bottom 0 + right 8px + margin auto 0 auto 0 + width 1em + height 1em + +script. + @browser = @parent + @folder = @opts.folder + + @onclick = ~> + @browser.move @folder diff --git a/src/web/app/mobile/tags/follow-button.tag b/src/web/app/mobile/tags/follow-button.tag new file mode 100644 index 0000000000..7cedbbee88 --- /dev/null +++ b/src/web/app/mobile/tags/follow-button.tag @@ -0,0 +1,108 @@ +mk-follow-button + button(if={ !init }, class={ wait: wait, follow: !user.is_following, unfollow: user.is_following }, + onclick={ onclick }, + disabled={ wait }) + i.fa.fa-minus(if={ !wait && user.is_following }) + i.fa.fa-plus(if={ !wait && !user.is_following }) + i.fa.fa-spinner.fa-pulse.fa-fw(if={ wait }) + | { user.is_following ? 'フォロー解除' : 'フォロー' } + div.init(if={ init }): i.fa.fa-spinner.fa-pulse.fa-fw + +style. + display block + + > button + > .init + display block + user-select none + cursor pointer + padding 0 16px + margin 0 + height inherit + font-size 16px + outline none + border solid 1px $theme-color + border-radius 4px + + * + pointer-events none + + &.follow + color $theme-color + background transparent + + &:hover + background rgba($theme-color, 0.1) + + &:active + background rgba($theme-color, 0.2) + + &.unfollow + color $theme-color-foreground + background $theme-color + + &.wait + cursor wait !important + opacity 0.7 + + &.init + cursor wait !important + opacity 0.7 + + > i + margin-right 4px + +script. + @mixin \api + @mixin \is-promise + @mixin \stream + + @user = null + @user-promise = if @is-promise @opts.user then @opts.user else Promise.resolve @opts.user + @init = true + @wait = false + + @on \mount ~> + @user-promise.then (user) ~> + @user = user + @init = false + @update! + @stream.on \follow @on-stream-follow + @stream.on \unfollow @on-stream-unfollow + + @on \unmount ~> + @stream.off \follow @on-stream-follow + @stream.off \unfollow @on-stream-unfollow + + @on-stream-follow = (user) ~> + if user.id == @user.id + @user = user + @update! + + @on-stream-unfollow = (user) ~> + if user.id == @user.id + @user = user + @update! + + @onclick = ~> + @wait = true + if @user.is_following + @api \following/delete do + user_id: @user.id + .then ~> + @user.is_following = false + .catch (err) -> + console.error err + .then ~> + @wait = false + @update! + else + @api \following/create do + user_id: @user.id + .then ~> + @user.is_following = true + .catch (err) -> + console.error err + .then ~> + @wait = false + @update! diff --git a/src/web/app/mobile/tags/home-timeline.tag b/src/web/app/mobile/tags/home-timeline.tag new file mode 100644 index 0000000000..1754bb2b07 --- /dev/null +++ b/src/web/app/mobile/tags/home-timeline.tag @@ -0,0 +1,40 @@ +mk-home-timeline + mk-timeline@timeline(init={ init }, more={ more }, empty={ '表示する投稿がありません。誰かしらをフォローするなどしましょう。' }) + +style. + display block + +script. + @mixin \api + @mixin \stream + + @init = new Promise (res, rej) ~> + @api \posts/timeline + .then (posts) ~> + res posts + @trigger \loaded + + @on \mount ~> + @stream.on \post @on-stream-post + @stream.on \follow @on-stream-follow + @stream.on \unfollow @on-stream-unfollow + + @on \unmount ~> + @stream.off \post @on-stream-post + @stream.off \follow @on-stream-follow + @stream.off \unfollow @on-stream-unfollow + + @more = ~> + @api \posts/timeline do + max_id: @refs.timeline.tail!.id + + @on-stream-post = (post) ~> + @is-empty = false + @update! + @refs.timeline.add-post post + + @on-stream-follow = ~> + @fetch! + + @on-stream-unfollow = ~> + @fetch! diff --git a/src/web/app/mobile/tags/home.tag b/src/web/app/mobile/tags/home.tag new file mode 100644 index 0000000000..ebcf8f0bb2 --- /dev/null +++ b/src/web/app/mobile/tags/home.tag @@ -0,0 +1,17 @@ +mk-home + mk-home-timeline@tl + +style. + display block + + > mk-home-timeline + max-width 600px + margin 0 auto + + @media (min-width 500px) + padding 16px + +script. + @on \mount ~> + @refs.tl.on \loaded ~> + @trigger \loaded diff --git a/src/web/app/mobile/tags/images-viewer.tag b/src/web/app/mobile/tags/images-viewer.tag new file mode 100644 index 0000000000..f9d774a124 --- /dev/null +++ b/src/web/app/mobile/tags/images-viewer.tag @@ -0,0 +1,25 @@ +mk-images-viewer + div.image@view(onclick={ click }) + img@img(src={ image.url + '?thumbnail&size=512' }, alt={ image.name }, title={ image.name }) + +style. + display block + padding 8px + overflow hidden + box-shadow 0 0 4px rgba(0, 0, 0, 0.2) + border-radius 4px + + > .image + + > img + display block + max-height 256px + max-width 100% + margin 0 auto + +script. + @images = @opts.images + @image = @images.0 + + @click = ~> + window.open @image.url diff --git a/src/web/app/mobile/tags/notification-preview.tag b/src/web/app/mobile/tags/notification-preview.tag new file mode 100644 index 0000000000..ee936df7ab --- /dev/null +++ b/src/web/app/mobile/tags/notification-preview.tag @@ -0,0 +1,117 @@ +mk-notification-preview(class={ notification.type }) + div.main(if={ notification.type == 'like' }) + img.avatar(src={ notification.user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + div.text + p + i.fa.fa-thumbs-o-up + | { notification.user.name } + p.post-ref { get-post-summary(notification.post) } + + div.main(if={ notification.type == 'repost' }) + img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + div.text + p + i.fa.fa-retweet + | { notification.post.user.name } + p.post-ref { get-post-summary(notification.post.repost) } + + div.main(if={ notification.type == 'quote' }) + img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + div.text + p + i.fa.fa-quote-left + | { notification.post.user.name } + p.post-preview { get-post-summary(notification.post) } + + div.main(if={ notification.type == 'follow' }) + img.avatar(src={ notification.user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + div.text + p + i.fa.fa-user-plus + | { notification.user.name } + + div.main(if={ notification.type == 'reply' }) + img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + div.text + p + i.fa.fa-reply + | { notification.post.user.name } + p.post-preview { get-post-summary(notification.post) } + + div.main(if={ notification.type == 'mention' }) + img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + div.text + p + i.fa.fa-at + | { notification.post.user.name } + p.post-preview { get-post-summary(notification.post) } + +style. + display block + margin 0 + padding 8px + color #fff + + > .main + word-wrap break-word + + &:after + content "" + display block + clear both + + img + display block + float left + min-width 36px + min-height 36px + max-width 36px + max-height 36px + border-radius 6px + + .text + float right + width calc(100% - 36px) + padding-left 8px + + p + margin 0 + + i + margin-right 4px + + .post-ref + + &:before, &:after + font-family FontAwesome + font-size 1em + font-weight normal + font-style normal + display inline-block + margin-right 3px + + &:before + content "\f10d" + + &:after + content "\f10e" + + &.like + .text p i + color #FFAC33 + + &.repost, &.quote + .text p i + color #77B255 + + &.follow + .text p i + color #53c7ce + + &.reply, &.mention + .text p i + color #fff + +script. + @mixin \get-post-summary + @notification = @opts.notification diff --git a/src/web/app/mobile/tags/notification.tag b/src/web/app/mobile/tags/notification.tag new file mode 100644 index 0000000000..afcc7441b4 --- /dev/null +++ b/src/web/app/mobile/tags/notification.tag @@ -0,0 +1,142 @@ +mk-notification(class={ notification.type }) + mk-time(time={ notification.created_at }) + + div.main(if={ notification.type == 'like' }) + a.avatar-anchor(href={ CONFIG.url + '/' + notification.user.username }) + img.avatar(src={ notification.user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + div.text + p + i.fa.fa-thumbs-o-up + a(href={ CONFIG.url + '/' + notification.user.username }) { notification.user.name } + a.post-ref(href={ CONFIG.url + '/' + notification.post.user.username + '/' + notification.post.id }) { get-post-summary(notification.post) } + + div.main(if={ notification.type == 'repost' }) + a.avatar-anchor(href={ CONFIG.url + '/' + notification.post.user.username }) + img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + div.text + p + i.fa.fa-retweet + a(href={ CONFIG.url + '/' + notification.post.user.username }) { notification.post.user.name } + a.post-ref(href={ CONFIG.url + '/' + notification.post.user.username + '/' + notification.post.id }) { get-post-summary(notification.post.repost) } + + div.main(if={ notification.type == 'quote' }) + a.avatar-anchor(href={ CONFIG.url + '/' + notification.post.user.username }) + img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + div.text + p + i.fa.fa-quote-left + a(href={ CONFIG.url + '/' + notification.post.user.username }) { notification.post.user.name } + a.post-preview(href={ CONFIG.url + '/' + notification.post.user.username + '/' + notification.post.id }) { get-post-summary(notification.post) } + + div.main(if={ notification.type == 'follow' }) + a.avatar-anchor(href={ CONFIG.url + '/' + notification.user.username }) + img.avatar(src={ notification.user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + div.text + p + i.fa.fa-user-plus + a(href={ CONFIG.url + '/' + notification.user.username }) { notification.user.name } + + div.main(if={ notification.type == 'reply' }) + a.avatar-anchor(href={ CONFIG.url + '/' + notification.post.user.username }) + img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + div.text + p + i.fa.fa-reply + a(href={ CONFIG.url + '/' + notification.post.user.username }) { notification.post.user.name } + a.post-preview(href={ CONFIG.url + '/' + notification.post.user.username + '/' + notification.post.id }) { get-post-summary(notification.post) } + + div.main(if={ notification.type == 'mention' }) + a.avatar-anchor(href={ CONFIG.url + '/' + notification.post.user.username }) + img.avatar(src={ notification.post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + div.text + p + i.fa.fa-at + a(href={ CONFIG.url + '/' + notification.post.user.username }) { notification.post.user.name } + a.post-preview(href={ CONFIG.url + '/' + notification.post.user.username + '/' + notification.post.id }) { get-post-summary(notification.post) } + +style. + display block + margin 0 + padding 16px + + > mk-time + display inline + position absolute + top 16px + right 12px + vertical-align top + color rgba(0, 0, 0, 0.6) + font-size 12px + + > .main + word-wrap break-word + + &:after + content "" + display block + clear both + + .avatar-anchor + display block + float left + + img + min-width 36px + min-height 36px + max-width 36px + max-height 36px + border-radius 6px + + .text + float right + width calc(100% - 36px) + padding-left 8px + + p + margin 0 + + i + margin-right 4px + + .post-preview + color rgba(0, 0, 0, 0.7) + + .post-ref + color rgba(0, 0, 0, 0.7) + + &:before, &:after + font-family FontAwesome + font-size 1em + font-weight normal + font-style normal + display inline-block + margin-right 3px + + &:before + content "\f10d" + + &:after + content "\f10e" + + &.like + .text p i + color #FFAC33 + + &.repost, &.quote + .text p i + color #77B255 + + &.follow + .text p i + color #53c7ce + + &.reply, &.mention + .text p i + color #555 + + .post-preview + color rgba(0, 0, 0, 0.7) + +script. + @mixin \get-post-summary + @notification = @opts.notification diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag new file mode 100644 index 0000000000..7510d59967 --- /dev/null +++ b/src/web/app/mobile/tags/notifications.tag @@ -0,0 +1,98 @@ +mk-notifications + div.notifications(if={ notifications.length != 0 }) + virtual(each={ notification, i in notifications }) + mk-notification(notification={ notification }) + + p.date(if={ i != notifications.length - 1 && notification._date != notifications[i + 1]._date }) + span + i.fa.fa-angle-up + | { notification._datetext } + span + i.fa.fa-angle-down + | { notifications[i + 1]._datetext } + + p.empty(if={ notifications.length == 0 && !loading }) + | ありません! + p.loading(if={ loading }) + i.fa.fa-spinner.fa-pulse.fa-fw + | 読み込んでいます + mk-ellipsis + +style. + display block + background #fff + + > .notifications + margin 0 auto + max-width 500px + + > mk-notification + border-bottom solid 1px rgba(0, 0, 0, 0.05) + + &:last-child + border-bottom none + + > .date + display block + margin 0 + line-height 32px + text-align center + font-size 0.8em + color #aaa + background #fdfdfd + border-bottom solid 1px rgba(0, 0, 0, 0.05) + + span + margin 0 16px + + i + margin-right 8px + + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > .loading + margin 0 + padding 16px + text-align center + color #aaa + + > i + margin-right 4px + +script. + @mixin \api + @mixin \stream + @mixin \get-post-summary + + @notifications = [] + @loading = true + + @on \mount ~> + @api \i/notifications + .then (notifications) ~> + @notifications = notifications + @loading = false + @update! + @trigger \loaded + .catch (err, text-status) -> + console.error err + + @stream.on \notification @on-notification + + @on \unmount ~> + @stream.off \notification @on-notification + + @on-notification = (notification) ~> + @notifications.unshift notification + @update! + + @on \update ~> + @notifications.for-each (notification) ~> + date = (new Date notification.created_at).get-date! + month = (new Date notification.created_at).get-month! + 1 + notification._date = date + notification._datetext = month + '月 ' + date + '日' diff --git a/src/web/app/mobile/tags/notify.tag b/src/web/app/mobile/tags/notify.tag new file mode 100644 index 0000000000..9dd93ccf25 --- /dev/null +++ b/src/web/app/mobile/tags/notify.tag @@ -0,0 +1,35 @@ +mk-notify + mk-notification-preview(notification={ opts.notification }) + +style. + display block + position fixed + z-index 1024 + bottom -64px + left 0 + width 100% + height 64px + pointer-events none + -webkit-backdrop-filter blur(2px) + backdrop-filter blur(2px) + background-color rgba(#000, 0.5) + +script. + @on \mount ~> + Velocity @root, { + bottom: \0px + } { + duration: 500ms + easing: \ease-out + } + + set-timeout ~> + Velocity @root, { + bottom: \-64px + } { + duration: 500ms + easing: \ease-out + complete: ~> + @unmount! + } + , 6000ms diff --git a/src/web/app/mobile/tags/page/drive.tag b/src/web/app/mobile/tags/page/drive.tag new file mode 100644 index 0000000000..9bef7e6648 --- /dev/null +++ b/src/web/app/mobile/tags/page/drive.tag @@ -0,0 +1,46 @@ +mk-drive-page + mk-ui@ui: mk-drive@browser(folder={ parent.opts.folder }, file={ parent.opts.file }) + +style. + display block + +script. + @mixin \ui + @mixin \ui-progress + + @on \mount ~> + document.title = 'Misskey Drive' + @ui.trigger \title '<i class="fa fa-cloud"></i>ドライブ' + + @refs.ui.refs.browser.on \begin-load ~> + @Progress.start! + + @refs.ui.refs.browser.on \loaded-mid ~> + @Progress.set 0.5 + + @refs.ui.refs.browser.on \loaded ~> + @Progress.done! + + @refs.ui.refs.browser.on \move-root ~> + @ui.trigger \title '<i class="fa fa-cloud"></i>ドライブ' + + # Rewrite URL + history.push-state null null '/i/drive' + + @refs.ui.refs.browser.on \cd (folder) ~> + # TODO: escape html characters in folder.name + @ui.trigger \title '<i class="fa fa-folder-open"></i>' + folder.name + + @refs.ui.refs.browser.on \move (folder) ~> + # Rewrite URL + history.push-state null null '/i/drive/folder/' + folder.id + + @refs.ui.refs.browser.on \open-file (file) ~> + # TODO: escape html characters in file.name + @ui.trigger \title '<mk-file-type-icon class="icon"></mk-file-type-icon>' + file.name + + # Rewrite URL + history.push-state null null '/i/drive/file/' + file.id + + riot.mount \mk-file-type-icon do + file: file diff --git a/src/web/app/mobile/tags/page/entrance.tag b/src/web/app/mobile/tags/page/entrance.tag new file mode 100644 index 0000000000..67d8bc9bbf --- /dev/null +++ b/src/web/app/mobile/tags/page/entrance.tag @@ -0,0 +1,57 @@ +mk-entrance + main + img(src='/_/resources/title.svg', alt='Misskey') + + mk-entrance-signin(if={ mode == 'signin' }) + mk-entrance-signup(if={ mode == 'signup' }) + div.introduction(if={ mode == 'introduction' }) + mk-introduction + button(onclick={ signin }) わかった + + footer + mk-copyright + +style. + display block + height 100% + + > main + display block + + > img + display block + width 130px + height 120px + margin 0 auto + + > .introduction + max-width 300px + margin 0 auto + color #666 + + > button + display block + margin 16px auto 0 auto + + > footer + > mk-copyright + margin 0 + text-align center + line-height 64px + font-size 10px + color rgba(#000, 0.5) + +script. + @mode = \signin + + @signup = ~> + @mode = \signup + @update! + + @signin = ~> + @mode = \signin + @update! + + @introduction = ~> + @mode = \introduction + @update! diff --git a/src/web/app/mobile/tags/page/entrance/signin.tag b/src/web/app/mobile/tags/page/entrance/signin.tag new file mode 100644 index 0000000000..484c414e8e --- /dev/null +++ b/src/web/app/mobile/tags/page/entrance/signin.tag @@ -0,0 +1,45 @@ +mk-entrance-signin + mk-signin + div.divider: span or + button.signup(onclick={ parent.signup }) 新規登録 + a.introduction(onclick={ parent.introduction }) Misskeyについて + +style. + display block + margin 0 auto + padding 0 8px + max-width 350px + text-align center + + > .signup + padding 16px + width 100% + font-size 1em + color #fff + background $theme-color + border-radius 3px + + > .divider + padding 16px 0 + text-align center + + &:after + content "" + display block + position absolute + top 50% + width 100% + height 1px + border-top solid 1px rgba(0, 0, 0, 0.1) + + > * + z-index 1 + padding 0 8px + color rgba(0, 0, 0, 0.5) + background #fdfdfd + + > .introduction + display inline-block + margin-top 16px + font-size 12px + color #666 diff --git a/src/web/app/mobile/tags/page/entrance/signup.tag b/src/web/app/mobile/tags/page/entrance/signup.tag new file mode 100644 index 0000000000..a28f85e634 --- /dev/null +++ b/src/web/app/mobile/tags/page/entrance/signup.tag @@ -0,0 +1,35 @@ +mk-entrance-signup + mk-signup + button.cancel(type='button', onclick={ parent.signin }, title='キャンセル'): i.fa.fa-times + +style. + display block + margin 0 auto + padding 0 8px + max-width 350px + + > .cancel + cursor pointer + display block + position absolute + top 0 + right 0 + z-index 1 + margin 0 + padding 0 + font-size 1.2em + color #999 + border none + outline none + box-shadow none + background transparent + transition opacity 0.1s ease + + &:hover + color #555 + + &:active + color #222 + + > i + padding 14px diff --git a/src/web/app/mobile/tags/page/home.tag b/src/web/app/mobile/tags/page/home.tag new file mode 100644 index 0000000000..c8d7729652 --- /dev/null +++ b/src/web/app/mobile/tags/page/home.tag @@ -0,0 +1,40 @@ +mk-home-page + mk-ui@ui: mk-home@home + +style. + display block + +script. + @mixin \i + @mixin \ui + @mixin \ui-progress + @mixin \stream + @mixin \get-post-summary + + @unread-count = 0 + + @on \mount ~> + document.title = 'Misskey' + @ui.trigger \title '<i class="fa fa-home"></i>ホーム' + + @Progress.start! + + @stream.on \post @on-stream-post + document.add-event-listener \visibilitychange @window-on-visibilitychange, false + + @refs.ui.refs.home.on \loaded ~> + @Progress.done! + + @on \unmount ~> + @stream.off \post @on-stream-post + document.remove-event-listener \visibilitychange @window-on-visibilitychange + + @on-stream-post = (post) ~> + if document.hidden and post.user_id !== @I.id + @unread-count++ + document.title = '(' + @unread-count + ') ' + @get-post-summary post + + @window-on-visibilitychange = ~> + if !document.hidden + @unread-count = 0 + document.title = 'Misskey' diff --git a/src/web/app/mobile/tags/page/new-post.tag b/src/web/app/mobile/tags/page/new-post.tag new file mode 100644 index 0000000000..21e00fc1f9 --- /dev/null +++ b/src/web/app/mobile/tags/page/new-post.tag @@ -0,0 +1,5 @@ +mk-new-post-page + mk-post-form@form + +style. + display block diff --git a/src/web/app/mobile/tags/page/notifications.tag b/src/web/app/mobile/tags/page/notifications.tag new file mode 100644 index 0000000000..9fb34dcd75 --- /dev/null +++ b/src/web/app/mobile/tags/page/notifications.tag @@ -0,0 +1,18 @@ +mk-notifications-page + mk-ui@ui: mk-notifications@notifications + +style. + display block + +script. + @mixin \ui + @mixin \ui-progress + + @on \mount ~> + document.title = 'Misskey | 通知' + @ui.trigger \title '<i class="fa fa-bell-o"></i>通知' + + @Progress.start! + + @refs.ui.refs.notifications.on \loaded ~> + @Progress.done! diff --git a/src/web/app/mobile/tags/page/post.tag b/src/web/app/mobile/tags/page/post.tag new file mode 100644 index 0000000000..1dc74d267a --- /dev/null +++ b/src/web/app/mobile/tags/page/post.tag @@ -0,0 +1,31 @@ +mk-post-page + mk-ui@ui: main: mk-post-detail@post(post={ parent.post }) + +style. + display block + + main + background #fff + + > mk-post-detail + width 100% + max-width 500px + margin 0 auto + +script. + @mixin \ui + @mixin \ui-progress + + @post = @opts.post + + @on \mount ~> + document.title = 'Misskey' + @ui.trigger \title '<i class="fa fa-sticky-note-o"></i>投稿' + + @Progress.start! + + @refs.ui.refs.post.on \post-fetched ~> + @Progress.set 0.5 + + @refs.ui.refs.post.on \loaded ~> + @Progress.done! diff --git a/src/web/app/mobile/tags/page/search.tag b/src/web/app/mobile/tags/page/search.tag new file mode 100644 index 0000000000..20de271f73 --- /dev/null +++ b/src/web/app/mobile/tags/page/search.tag @@ -0,0 +1,19 @@ +mk-search-page + mk-ui@ui: mk-search@search(query={ parent.opts.query }) + +style. + display block + +script. + @mixin \ui + @mixin \ui-progress + + @on \mount ~> + document.title = '検索: ' + @opts.query + ' | Misskey' + # TODO: クエリをHTMLエスケープ + @ui.trigger \title '<i class="fa fa-search"></i>' + @opts.query + + @Progress.start! + + @refs.ui.refs.search.on \loaded ~> + @Progress.done! diff --git a/src/web/app/mobile/tags/page/user-followers.tag b/src/web/app/mobile/tags/page/user-followers.tag new file mode 100644 index 0000000000..e7e9a6fd1e --- /dev/null +++ b/src/web/app/mobile/tags/page/user-followers.tag @@ -0,0 +1,31 @@ +mk-user-followers-page + mk-ui@ui: mk-user-followers@list(if={ !parent.fetching }, user={ parent.user }) + +style. + display block + +script. + @mixin \ui + @mixin \ui-progress + @mixin \api + + @fetching = true + @user = null + + @on \mount ~> + @Progress.start! + + @api \users/show do + username: @opts.user + .then (user) ~> + @user = user + @fetching = false + + document.title = user.name + 'のフォロワー | Misskey' + # TODO: ユーザー名をエスケープ + @ui.trigger \title '<img src="' + user.avatar_url + '?thumbnail&size=64">' + user.name + 'のフォロー' + + @update! + + @refs.ui.refs.list.on \loaded ~> + @Progress.done! diff --git a/src/web/app/mobile/tags/page/user-following.tag b/src/web/app/mobile/tags/page/user-following.tag new file mode 100644 index 0000000000..a74ba97b72 --- /dev/null +++ b/src/web/app/mobile/tags/page/user-following.tag @@ -0,0 +1,31 @@ +mk-user-following-page + mk-ui@ui: mk-user-following@list(if={ !parent.fetching }, user={ parent.user }) + +style. + display block + +script. + @mixin \ui + @mixin \ui-progress + @mixin \api + + @fetching = true + @user = null + + @on \mount ~> + @Progress.start! + + @api \users/show do + username: @opts.user + .then (user) ~> + @user = user + @fetching = false + + document.title = user.name + 'のフォロー | Misskey' + # TODO: ユーザー名をエスケープ + @ui.trigger \title '<img src="' + user.avatar_url + '?thumbnail&size=64">' + user.name + 'のフォロー' + + @update! + + @refs.ui.refs.list.on \loaded ~> + @Progress.done! diff --git a/src/web/app/mobile/tags/page/user.tag b/src/web/app/mobile/tags/page/user.tag new file mode 100644 index 0000000000..9667abfd14 --- /dev/null +++ b/src/web/app/mobile/tags/page/user.tag @@ -0,0 +1,20 @@ +mk-user-page + mk-ui@ui: mk-user@user(user={ parent.user }, page={ parent.opts.page }) + +style. + display block + +script. + @mixin \ui + @mixin \ui-progress + + @user = @opts.user + + @on \mount ~> + @Progress.start! + + @refs.ui.refs.user.on \loaded (user) ~> + @Progress.done! + document.title = user.name + ' | Misskey' + # TODO: ユーザー名をエスケープ + @ui.trigger \title '<i class="fa fa-user"></i>' + user.name diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag new file mode 100644 index 0000000000..c7eb091ce6 --- /dev/null +++ b/src/web/app/mobile/tags/post-detail.tag @@ -0,0 +1,415 @@ +mk-post-detail + + div.fetching(if={ fetching }) + mk-ellipsis-icon + + div.main(if={ !fetching }) + + button.read-more(if={ p.reply_to && p.reply_to.reply_to_id && context == null }, onclick={ load-context }, disabled={ loading-context }) + i.fa.fa-ellipsis-v(if={ !loading-context }) + i.fa.fa-spinner.fa-pulse(if={ loading-context }) + + div.context + virtual(each={ post in context }) + mk-post-preview(post={ post }) + + div.reply-to(if={ p.reply_to }) + mk-post-preview(post={ p.reply_to }) + + div.repost(if={ is-repost }) + p + a.avatar-anchor(href={ CONFIG.url + '/' + post.user.username }): img.avatar(src={ post.user.avatar_url + '?thumbnail&size=32' }, alt='avatar') + i.fa.fa-retweet + a.name(href={ CONFIG.url + '/' + post.user.username }) { post.user.name } + | がRepost + + article + a.avatar-anchor(href={ CONFIG.url + '/' + p.user.username }) + img.avatar(src={ p.user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + header + a.name(href={ CONFIG.url + '/' + p.user.username }) + | { p.user.name } + span.username + | @{ p.user.username } + div.body + div.text@text + div.media(if={ p.media }) + virtual(each={ file in p.media }) + img(src={ file.url + '?thumbnail&size=512' }, alt={ file.name }, title={ file.name }) + a.time(href={ url }) + mk-time(time={ p.created_at }, mode='detail') + footer + button(onclick={ reply }, title='返信') + i.fa.fa-reply + p.count(if={ p.replies_count > 0 }) { p.replies_count } + button(onclick={ repost }, title='Repost') + i.fa.fa-retweet + p.count(if={ p.repost_count > 0 }) { p.repost_count } + button(class={ liked: p.is_liked }, onclick={ like }, title='善哉') + i.fa.fa-thumbs-o-up + p.count(if={ p.likes_count > 0 }) { p.likes_count } + button(onclick={ NotImplementedException }): i.fa.fa-ellipsis-h + div.reposts-and-likes + div.reposts(if={ reposts && reposts.length > 0 }) + header + a { p.repost_count } + p Repost + ol.users + li.user(each={ reposts }) + a.avatar-anchor(href={ CONFIG.url + '/' + user.username }, title={ user.name }) + img.avatar(src={ user.avatar_url + '?thumbnail&size=32' }, alt='') + div.likes(if={ likes && likes.length > 0 }) + header + a { p.likes_count } + p いいね + ol.users + li.user(each={ likes }) + a.avatar-anchor(href={ CONFIG.url + '/' + username }, title={ name }) + img.avatar(src={ avatar_url + '?thumbnail&size=32' }, alt='') + + div.replies + virtual(each={ post in replies }) + mk-post-detail-sub(post={ post }) + +style. + display block + margin 0 + padding 0 + + > .fetching + padding 64px 0 + + > .main + + > .read-more + display block + margin 0 + padding 10px 0 + width 100% + font-size 1em + text-align center + color #999 + cursor pointer + background #fafafa + outline none + border none + border-bottom solid 1px #eef0f2 + border-radius 6px 6px 0 0 + box-shadow none + + &:hover + background #f6f6f6 + + &:active + background #f0f0f0 + + &:disabled + color #ccc + + > .context + > * + border-bottom 1px solid #eef0f2 + + > .repost + color #9dbb00 + background linear-gradient(to bottom, #edfde2 0%, #fff 100%) + + > p + margin 0 + padding 16px 32px + + .avatar-anchor + display inline-block + + .avatar + vertical-align bottom + min-width 28px + min-height 28px + max-width 28px + max-height 28px + margin 0 8px 0 0 + border-radius 6px + + i + margin-right 4px + + .name + font-weight bold + + & + article + padding-top 8px + + > .reply-to + border-bottom 1px solid #eef0f2 + + > article + padding 14px 16px 9px 16px + + @media (min-width 500px) + padding 28px 32px 18px 32px + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + + > .avatar + display block + width 54px + height 54px + margin 0 + border-radius 8px + vertical-align bottom + + @media (min-width 500px) + width 60px + height 60px + + > header + position absolute + top 18px + left 80px + width calc(100% - 80px) + + @media (min-width 500px) + top 28px + left 108px + width calc(100% - 108px) + + > .name + display inline-block + margin 0 + color #777 + font-size 16px + font-weight bold + text-align left + text-decoration none + + &:hover + text-decoration underline + + > .username + display block + text-align left + margin 0 + color #ccc + + > .body + padding 8px 0 + + > .text + cursor default + display block + margin 0 + padding 0 + word-wrap break-word + font-size 16px + color #717171 + + @media (min-width 500px) + font-size 24px + + > mk-url-preview + margin-top 8px + + > .media + > img + display block + max-width 100% + + > .time + font-size 16px + color #c0c0c0 + + > footer + font-size 1.2em + + > button + margin 0 28px 0 0 + padding 8px + background transparent + border none + box-shadow none + font-size 1em + color #ddd + cursor pointer + + &:hover + color #666 + + > .count + display inline + margin 0 0 0 8px + color #999 + + &.liked + color $theme-color + + > .reposts-and-likes + display flex + justify-content center + padding 0 + margin 16px 0 + + &:empty + display none + + > .reposts + > .likes + display flex + flex 1 1 + padding 0 + border-top solid 1px #F2EFEE + + > header + flex 1 1 80px + max-width 80px + padding 8px 5px 0px 10px + + > a + display block + font-size 1.5em + line-height 1.4em + + > p + display block + margin 0 + font-size 0.7em + line-height 1em + font-weight normal + color #a0a2a5 + + > .users + display block + flex 1 1 + margin 0 + padding 10px 10px 10px 5px + list-style none + + > .user + display block + float left + margin 4px + padding 0 + + > .avatar-anchor + display:block + + > .avatar + vertical-align bottom + width 24px + height 24px + border-radius 4px + + > .reposts + .likes + margin-left 16px + + > .replies + > * + border-top 1px solid #eef0f2 + +script. + @mixin \api + @mixin \text + @mixin \get-post-summary + @mixin \open-post-form + + @fetching = true + @loading-context = false + @content = null + @post = null + + @on \mount ~> + @api \posts/show do + post_id: @opts.post + .then (post) ~> + @post = post + @is-repost = @post.repost? + @p = if @is-repost then @post.repost else @post + @summary = @get-post-summary @p + @trigger \loaded + @fetching = false + @update! + + if @p.text? + tokens = @analyze @p.text + @refs.text.innerHTML = @compile tokens + + @refs.text.children.for-each (e) ~> + if e.tag-name == \MK-URL + riot.mount e + + # URLをプレビュー + tokens + .filter (t) -> t.type == \link + .map (t) ~> + @preview = @refs.text.append-child document.create-element \mk-url-preview + riot.mount @preview, do + url: t.content + + # Get likes + @api \posts/likes do + post_id: @p.id + limit: 8 + .then (likes) ~> + @likes = likes + @update! + + # Get reposts + @api \posts/reposts do + post_id: @p.id + limit: 8 + .then (reposts) ~> + @reposts = reposts + @update! + + # Get replies + @api \posts/replies do + post_id: @p.id + limit: 8 + .then (replies) ~> + @replies = replies + @update! + + @reply = ~> + @open-post-form do + reply: @p + + @repost = ~> + text = window.prompt '「' + @summary + '」をRepost' + if text? + @api \posts/create do + repost_id: @p.id + text: if text == '' then undefined else text + + @like = ~> + if @p.is_liked + @api \posts/likes/delete do + post_id: @p.id + .then ~> + @p.is_liked = false + @update! + else + @api \posts/likes/create do + post_id: @p.id + .then ~> + @p.is_liked = true + @update! + + @load-context = ~> + @loading-context = true + + # Get context + @api \posts/context do + post_id: @p.reply_to_id + .then (context) ~> + @context = context.reverse! + @loading-context = false + @update! diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag new file mode 100644 index 0000000000..759a0820b8 --- /dev/null +++ b/src/web/app/mobile/tags/post-form.tag @@ -0,0 +1,254 @@ +mk-post-form + header: div + button.cancel(onclick={ cancel }): i.fa.fa-times + div + span.text-count(class={ over: refs.text.value.length > 300 }) { 300 - refs.text.value.length } + button.submit(onclick={ post }) 投稿 + div.form + mk-post-preview(if={ opts.reply }, post={ opts.reply }) + textarea@text(disabled={ wait }, oninput={ update }, onkeypress={ onkeypress }, onpaste={ onpaste }, placeholder={ opts.reply ? 'この投稿への返信...' : 'いまどうしてる?' }) + div.attaches(if={ files.length != 0 }) + ul.files@attaches + li.file(each={ files }) + div.img(style='background-image: url({ url + "?thumbnail&size=64" })', title={ name }) + li.add(if={ files.length < 4 }, title='PCからファイルを添付', onclick={ select-file }): i.fa.fa-plus + mk-uploader@uploader + button@upload(onclick={ select-file }): i.fa.fa-upload + button@drive(onclick={ select-file-from-drive }): i.fa.fa-cloud + input@file(type='file', accept='image/*', multiple, onchange={ change-file }) + +style. + display block + padding-top 50px + + > header + position fixed + z-index 1000 + top 0 + left 0 + width 100% + height 50px + background #fff + + > div + max-width 500px + margin 0 auto + + > .cancel + width 50px + line-height 50px + font-size 24px + color #555 + + > div + position absolute + top 0 + right 0 + + > .text-count + line-height 50px + color #657786 + + > .submit + margin 8px + padding 0 16px + line-height 34px + color $theme-color-foreground + background $theme-color + border-radius 4px + + &:disabled + opacity 0.7 + + > .form + max-width 500px + margin 0 auto + + > mk-post-preview + padding 16px + + > .attaches + + > .files + display block + margin 0 + padding 4px + list-style none + + &:after + content "" + display block + clear both + + > .file + display block + float left + margin 4px + padding 0 + 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 + + > .add + display block + float left + margin 4px + padding 0 + border dashed 2px rgba($theme-color, 0.2) + cursor pointer + + &:hover + border-color rgba($theme-color, 0.3) + + > i + color rgba($theme-color, 0.4) + + > i + display block + width 60px + height 60px + line-height 60px + text-align center + font-size 1.2em + color rgba($theme-color, 0.2) + + > mk-uploader + margin 8px 0 0 0 + padding 8px + + > [ref='file'] + display none + + > [ref='text'] + display block + padding 12px + margin 0 + width 100% + max-width 100% + min-width 100% + min-height 80px + font-size 16px + color #333 + border none + border-bottom solid 1px #ddd + border-radius 0 + + &:disabled + opacity 0.5 + + > [ref='upload'] + > [ref='drive'] + display inline-block + padding 0 + margin 0 + width 48px + height 48px + font-size 20px + color #657786 + background transparent + outline none + border none + border-radius 0 + box-shadow none + +script. + @mixin \api + + @wait = false + @uploadings = [] + @files = [] + + @on \mount ~> + @refs.uploader.on \uploaded (file) ~> + @add-file file + + @refs.uploader.on \change-uploads (uploads) ~> + @trigger \change-uploading-files uploads + + @refs.text.focus! + + @onkeypress = (e) ~> + if (e.char-code == 10 || e.char-code == 13) && e.ctrl-key + @post! + else + return true + + @onpaste = (e) ~> + data = e.clipboard-data + items = data.items + for i from 0 to items.length - 1 + item = items[i] + switch (item.kind) + | \file => + @upload item.get-as-file! + return true + + @select-file = ~> + @refs.file.click! + + @select-file-from-drive = ~> + browser = document.body.append-child document.create-element \mk-drive-selector + browser = riot.mount browser, do + multiple: true + .0 + browser.on \selected (files) ~> + files.for-each @add-file + + @change-file = ~> + files = @refs.file.files + for i from 0 to files.length - 1 + file = files.item i + @upload file + + @upload = (file) ~> + @refs.uploader.upload file + + @add-file = (file) ~> + file._remove = ~> + @files = @files.filter (x) -> x.id != file.id + @trigger \change-files @files + @update! + + @files.push file + @trigger \change-files @files + @update! + + @post = ~> + @wait = true + + files = if @files? and @files.length > 0 + then @files.map (f) -> f.id + else undefined + + @api \posts/create do + text: @refs.text.value + media_ids: files + reply_to_id: if @opts.reply? then @opts.reply.id else undefined + .then (data) ~> + @trigger \post + @unmount! + .catch (err) ~> + console.error err + #@opts.ui.trigger \notification 'Error!' + @wait = false + @update! + + @cancel = ~> + @trigger \cancel + @unmount! diff --git a/src/web/app/mobile/tags/post-preview.tag b/src/web/app/mobile/tags/post-preview.tag new file mode 100644 index 0000000000..e15b2be244 --- /dev/null +++ b/src/web/app/mobile/tags/post-preview.tag @@ -0,0 +1,89 @@ +mk-post-preview + article + a.avatar-anchor(href={ CONFIG.url + '/' + post.user.username }) + img.avatar(src={ post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + div.main + header + a.name(href={ CONFIG.url + '/' + post.user.username }) + | { post.user.name } + span.username + | @{ post.user.username } + a.time(href={ CONFIG.url + '/' + post.user.username + '/' + post.id }) + mk-time(time={ post.created_at }) + div.body + mk-sub-post-content.text(post={ post }) + +style. + display block + margin 0 + padding 0 + font-size 0.9em + background #fff + + > article + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + float left + margin 0 12px 0 0 + + > .avatar + display block + width 48px + height 48px + margin 0 + border-radius 8px + vertical-align bottom + + > .main + float left + width calc(100% - 60px) + + > header + margin-bottom 4px + white-space nowrap + + > .name + display inline + margin 0 + padding 0 + color #607073 + font-size 1em + font-weight 700 + text-align left + text-decoration none + + &:hover + text-decoration underline + + > .username + text-align left + margin 0 0 0 8px + color #d1d8da + + > .time + position absolute + top 0 + right 0 + color #b2b8bb + + > .body + + > .text + cursor default + margin 0 + padding 0 + font-size 1.1em + color #717171 + +script. + @post = @opts.post diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag new file mode 100644 index 0000000000..4b1b12af27 --- /dev/null +++ b/src/web/app/mobile/tags/search-posts.tag @@ -0,0 +1,29 @@ +mk-search-posts + mk-timeline(init={ init }, more={ more }, empty={ '「' + query + '」に関する投稿は見つかりませんでした。' }) + +style. + display block + background #fff + +script. + @mixin \api + + @max = 30 + @offset = 0 + + @query = @opts.query + @with-media = @opts.with-media + + @init = new Promise (res, rej) ~> + @api \posts/search do + query: @query + .then (posts) ~> + res posts + @trigger \loaded + + @more = ~> + @offset += @max + @api \posts/search do + query: @query + max: @max + offset: @offset diff --git a/src/web/app/mobile/tags/search.tag b/src/web/app/mobile/tags/search.tag new file mode 100644 index 0000000000..bf2299cc9b --- /dev/null +++ b/src/web/app/mobile/tags/search.tag @@ -0,0 +1,12 @@ +mk-search + mk-search-posts@posts(query={ query }) + +style. + display block + +script. + @query = @opts.query + + @on \mount ~> + @refs.posts.on \loaded ~> + @trigger \loaded diff --git a/src/web/app/mobile/tags/stream-indicator.tag b/src/web/app/mobile/tags/stream-indicator.tag new file mode 100644 index 0000000000..2eb5889ca6 --- /dev/null +++ b/src/web/app/mobile/tags/stream-indicator.tag @@ -0,0 +1,59 @@ +mk-stream-indicator + p(if={ state == 'initializing' }) + i.fa.fa-spinner.fa-spin + span + | 接続中 + mk-ellipsis + p(if={ state == 'reconnecting' }) + i.fa.fa-spinner.fa-spin + span + | 切断されました 接続中 + mk-ellipsis + p(if={ state == 'connected' }) + i.fa.fa-check + span 接続完了 + +style. + display block + pointer-events none + position fixed + z-index 16384 + bottom 8px + right 8px + margin 0 + padding 6px 12px + font-size 0.9em + color #fff + background rgba(0, 0, 0, 0.8) + + > p + display block + margin 0 + + > i + margin-right 0.25em + +script. + @mixin \stream + + @on \before-mount ~> + @state = @get-stream-state! + + if @state == \connected + @root.style.opacity = 0 + + @stream-state-ev.on \connected ~> + @state = @get-stream-state! + @update! + set-timeout ~> + Velocity @root, { + opacity: 0 + } 200ms \linear + , 1000ms + + @stream-state-ev.on \closed ~> + @state = @get-stream-state! + @update! + Velocity @root, { + opacity: 1 + } 0ms diff --git a/src/web/app/mobile/tags/sub-post-content.tag b/src/web/app/mobile/tags/sub-post-content.tag new file mode 100644 index 0000000000..595f63d794 --- /dev/null +++ b/src/web/app/mobile/tags/sub-post-content.tag @@ -0,0 +1,36 @@ +mk-sub-post-content + div.body + a.reply(if={ post.reply_to_id }): i.fa.fa-reply + span@text + a.quote(if={ post.repost_id }, href={ '/post:' + post.repost_id }) RP: ... + details(if={ post.media }) + summary ({ post.media.length }枚の画像) + mk-images-viewer(images={ post.media }) + +style. + display block + word-wrap break-word + + > .body + > .reply + margin-right 6px + color #717171 + + > .quote + margin-left 4px + font-style oblique + color #a0bf46 + +script. + @mixin \text + + @post = @opts.post + + @on \mount ~> + if @post.text? + tokens = @analyze @post.text + @refs.text.innerHTML = @compile tokens, false + + @refs.text.children.for-each (e) ~> + if e.tag-name == \MK-URL + riot.mount e diff --git a/src/web/app/mobile/tags/timeline-post-sub.tag b/src/web/app/mobile/tags/timeline-post-sub.tag new file mode 100644 index 0000000000..920503ebcc --- /dev/null +++ b/src/web/app/mobile/tags/timeline-post-sub.tag @@ -0,0 +1,99 @@ +mk-timeline-post-sub + article + a.avatar-anchor(href={ '/' + post.user.username }) + img.avatar(src={ post.user.avatar_url + '?thumbnail&size=96' }, alt='avatar') + div.main + header + a.name(href={ '/' + post.user.username }) + | { post.user.name } + span.username + | @{ post.user.username } + a.created-at(href={ '/' + post.user.username + '/' + post.id }) + mk-time(time={ post.created_at }) + div.body + mk-sub-post-content.text(post={ post }) + +style. + display block + margin 0 + padding 0 + font-size 0.9em + + > article + padding 16px + + &:after + content "" + display block + clear both + + &:hover + > .main > footer > button + color #888 + + > .avatar-anchor + display block + float left + margin 0 10px 0 0 + + @media (min-width 500px) + margin-right 16px + + > .avatar + display block + width 44px + height 44px + margin 0 + border-radius 8px + vertical-align bottom + + @media (min-width 500px) + width 52px + height 52px + + > .main + float left + width calc(100% - 54px) + + @media (min-width 500px) + width calc(100% - 68px) + + > header + margin-bottom 4px + white-space nowrap + + > .name + display inline + margin 0 + padding 0 + color #607073 + font-size 1em + font-weight 700 + text-align left + text-decoration none + + &:hover + text-decoration underline + + > .username + text-align left + margin 0 0 0 8px + color #d1d8da + + > .created-at + position absolute + top 0 + right 0 + color #b2b8bb + + > .body + + > .text + cursor default + margin 0 + padding 0 + font-size 1.1em + color #717171 + +script. + @post = @opts.post diff --git a/src/web/app/mobile/tags/timeline-post.tag b/src/web/app/mobile/tags/timeline-post.tag new file mode 100644 index 0000000000..a71fab26f0 --- /dev/null +++ b/src/web/app/mobile/tags/timeline-post.tag @@ -0,0 +1,296 @@ +mk-timeline-post(class={ repost: is-repost }) + + div.reply-to(if={ p.reply_to }) + mk-timeline-post-sub(post={ p.reply_to }) + + div.repost(if={ is-repost }) + p + a.avatar-anchor(href={ CONFIG.url + '/' + post.user.username }): img.avatar(src={ post.user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + i.fa.fa-retweet + a.name(href={ CONFIG.url + '/' + post.user.username }) { post.user.name } + | がRepost + mk-time(time={ post.created_at }) + + article + a.avatar-anchor(href={ CONFIG.url + '/' + p.user.username }) + img.avatar(src={ p.user.avatar_url + '?thumbnail&size=96' }, alt='avatar') + div.main + header + a.name(href={ CONFIG.url + '/' + p.user.username }) + | { p.user.name } + span.username + | @{ p.user.username } + a.created-at(href={ url }) + mk-time(time={ p.created_at }) + div.body + div.text + a.reply(if={ p.reply_to }): i.fa.fa-reply + soan@text + a.quote(if={ p.repost != null }) RP: + div.media(if={ p.media }) + mk-images-viewer(images={ p.media }) + div.repost(if={ p.repost }) + i.fa.fa-quote-right.fa-flip-horizontal + mk-post-preview.repost(post={ p.repost }) + footer + button(onclick={ reply }) + i.fa.fa-reply + p.count(if={ p.replies_count > 0 }) { p.replies_count } + button(onclick={ repost }, title='Repost') + i.fa.fa-retweet + p.count(if={ p.repost_count > 0 }) { p.repost_count } + button(class={ liked: p.is_liked }, onclick={ like }) + i.fa.fa-thumbs-o-up + p.count(if={ p.likes_count > 0 }) { p.likes_count } + +style. + display block + margin 0 + padding 0 + font-size 12px + + @media (min-width 350px) + font-size 14px + + @media (min-width 500px) + font-size 16px + + > .repost + color #9dbb00 + background linear-gradient(to bottom, #edfde2 0%, #fff 100%) + + > p + margin 0 + padding 8px 16px + line-height 28px + + @media (min-width 500px) + padding 16px + + .avatar-anchor + display inline-block + + .avatar + vertical-align bottom + width 28px + height 28px + margin 0 8px 0 0 + border-radius 6px + + i + margin-right 4px + + .name + font-weight bold + + > mk-time + position absolute + top 8px + right 16px + font-size 0.9em + line-height 28px + + @media (min-width 500px) + top 16px + + & + article + padding-top 8px + + > .reply-to + background rgba(0, 0, 0, 0.0125) + + > mk-post-preview + background transparent + + > article + padding 14px 16px 9px 16px + + &:after + content "" + display block + clear both + + > .avatar-anchor + display block + float left + margin 0 10px 0 0 + + @media (min-width 500px) + margin-right 16px + + > .avatar + display block + width 48px + height 48px + margin 0 + border-radius 6px + vertical-align bottom + + @media (min-width 500px) + width 58px + height 58px + border-radius 8px + + > .main + float left + width calc(100% - 58px) + + @media (min-width 500px) + width calc(100% - 74px) + + > header + white-space nowrap + + @media (min-width 500px) + margin-bottom 2px + + > .name + display inline + margin 0 + padding 0 + color #777 + font-size 1em + font-weight 700 + text-align left + text-decoration none + + &:hover + text-decoration underline + + > .username + text-align left + margin 0 0 0 8px + color #ccc + + > .created-at + position absolute + top 0 + right 0 + font-size 0.9em + color #c0c0c0 + + > .body + + > .text + cursor default + display block + margin 0 + padding 0 + word-wrap break-word + font-size 1.1em + color #717171 + + mk-url-preview + margin-top 8px + + > .reply + margin-right 8px + color #717171 + + > .quote + margin-left 4px + font-style oblique + color #a0bf46 + + > .media + > img + display block + max-width 100% + + > .repost + margin 8px 0 + + > i:first-child + position absolute + top -8px + left -8px + z-index 1 + color #c0dac6 + font-size 28px + background #fff + + > mk-post-preview + padding 16px + border dashed 1px #c0dac6 + border-radius 8px + + > footer + > button + margin 0 28px 0 0 + padding 8px + background transparent + border none + box-shadow none + font-size 1em + color #ddd + cursor pointer + + &:hover + color #666 + + > .count + display inline + margin 0 0 0 8px + color #999 + + &.liked + color $theme-color + +script. + @mixin \api + @mixin \text + @mixin \get-post-summary + @mixin \open-post-form + + @post = @opts.post + @is-repost = @post.repost? and !@post.text? + @p = if @is-repost then @post.repost else @post + @summary = @get-post-summary @p + @url = CONFIG.url + '/' + @p.user.username + '/' + @p.id + + @on \mount ~> + if @p.text? + tokens = if @p._highlight? + then @analyze @p._highlight + else @analyze @p.text + + @refs.text.innerHTML = if @p._highlight? + then @compile tokens, true, false + else @compile tokens + + @refs.text.children.for-each (e) ~> + if e.tag-name == \MK-URL + riot.mount e + + # URLをプレビュー + tokens + .filter (t) -> t.type == \link + .map (t) ~> + @preview = @refs.text.append-child document.create-element \mk-url-preview + riot.mount @preview, do + url: t.content + + @reply = ~> + @open-post-form do + reply: @p + + @repost = ~> + text = window.prompt '「' + @summary + '」をRepost' + if text? + @api \posts/create do + repost_id: @p.id + text: if text == '' then undefined else text + + @like = ~> + if @p.is_liked + @api \posts/likes/delete do + post_id: @p.id + .then ~> + @p.is_liked = false + @update! + else + @api \posts/likes/create do + post_id: @p.id + .then ~> + @p.is_liked = true + @update! diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag new file mode 100644 index 0000000000..7114824872 --- /dev/null +++ b/src/web/app/mobile/tags/timeline.tag @@ -0,0 +1,128 @@ +mk-timeline + div.init(if={ init }) + i.fa.fa-spinner.fa-pulse + | 読み込んでいます + div.empty(if={ !init && posts.length == 0 }) + i.fa.fa-comments-o + | { opts.empty || '表示するものがありません' } + virtual(each={ post, i in posts }) + mk-timeline-post(post={ post }) + p.date(if={ i != posts.length - 1 && post._date != posts[i + 1]._date }) + span + i.fa.fa-angle-up + | { post._datetext } + span + i.fa.fa-angle-down + | { posts[i + 1]._datetext } + footer(if={ !init }) + button(if={ can-fetch-more }, onclick={ more }, disabled={ fetching }) + span(if={ !fetching }) もっとみる + span(if={ fetching }) + | 読み込み中 + mk-ellipsis + +style. + display block + background #fff + background-clip content-box + overflow hidden + + > .init + padding 64px 0 + text-align center + color #999 + + > i + margin-right 4px + + > .empty + margin 0 auto + padding 32px + max-width 400px + text-align center + color #999 + + > i + display block + margin-bottom 16px + font-size 3em + color #ccc + + > mk-timeline-post + border-bottom solid 1px #eaeaea + + &:last-of-type + border-bottom none + + > .date + display block + margin 0 + line-height 32px + text-align center + font-size 0.9em + color #aaa + background #fdfdfd + border-bottom solid 1px #eaeaea + + span + margin 0 16px + + i + margin-right 8px + + > footer + text-align center + border-top solid 1px #eaeaea + border-bottom-left-radius 4px + border-bottom-right-radius 4px + + > button + margin 0 + padding 16px + width 100% + color $theme-color + + &:disabled + opacity 0.7 + +script. + @posts = [] + @init = true + @fetching = false + @can-fetch-more = true + + @on \mount ~> + @opts.init.then (posts) ~> + @init = false + @set-posts posts + + @on \update ~> + @posts.for-each (post) ~> + date = (new Date post.created_at).get-date! + month = (new Date post.created_at).get-month! + 1 + post._date = date + post._datetext = month + '月 ' + date + '日' + + @more = ~> + if @init or @fetching or @posts.length == 0 then return + @fetching = true + @update! + @opts.more!.then (posts) ~> + @fetching = false + @prepend-posts posts + + @set-posts = (posts) ~> + @posts = posts + @update! + + @prepend-posts = (posts) ~> + posts.for-each (post) ~> + @posts.push post + @update! + + @add-post = (post) ~> + @posts.unshift post + @update! + + @tail = ~> + @posts[@posts.length - 1] diff --git a/src/web/app/mobile/tags/ui-header.tag b/src/web/app/mobile/tags/ui-header.tag new file mode 100644 index 0000000000..7105d065f8 --- /dev/null +++ b/src/web/app/mobile/tags/ui-header.tag @@ -0,0 +1,98 @@ +mk-ui-header + mk-special-message + div.main + div.backdrop + div.content + button.nav#hamburger: i.fa.fa-bars + h1@title Misskey + button.post(onclick={ post }): i.fa.fa-pencil + +style. + $height = 48px + + display block + position fixed + top 0 + z-index 1024 + width 100% + box-shadow 0 1px 0 rgba(#000, 0.075) + + > .main + color rgba(#000, 0.6) + + > .backdrop + position absolute + top 0 + z-index 1023 + width 100% + height $height + -webkit-backdrop-filter blur(12px) + backdrop-filter blur(12px) + background-color rgba(#fff, 0.75) + + > .content + z-index 1024 + + > h1 + display block + margin 0 auto + padding 0 + width 100% + max-width calc(100% - 112px) + text-align center + font-size 1.1em + font-weight normal + line-height $height + white-space nowrap + overflow hidden + text-overflow ellipsis + + > i + margin-right 8px + + > img + display inline-block + vertical-align bottom + width ($height - 16px) + height ($height - 16px) + margin 8px + border-radius 6px + + > .nav + display block + position absolute + top 0 + left 0 + width $height + font-size 1.4em + line-height $height + border-right solid 1px rgba(#000, 0.1) + + > i + transition all 0.2s ease + + > .post + display block + position absolute + top 0 + right 0 + width $height + text-align center + font-size 1.4em + color inherit + line-height $height + border-left solid 1px rgba(#000, 0.1) + +script. + @mixin \ui + @mixin \open-post-form + + @on \mount ~> + @opts.ready! + + @ui.one \title (title) ~> + if @refs.title? + @refs.title.innerHTML = title + + @post = ~> + @open-post-form! diff --git a/src/web/app/mobile/tags/ui-nav.tag b/src/web/app/mobile/tags/ui-nav.tag new file mode 100644 index 0000000000..2c551b30ad --- /dev/null +++ b/src/web/app/mobile/tags/ui-nav.tag @@ -0,0 +1,169 @@ +mk-ui-nav + div.body: div.content + a.me(if={ SIGNIN }, href={ CONFIG.url + '/' + I.username }) + img.avatar(src={ I.avatar_url + '?thumbnail&size=128' }, alt='avatar') + p.name { I.name } + div.links + ul + li.post: a(href='/i/post') + i.icon.fa.fa-pencil-square-o + | 新規投稿 + i.angle.fa.fa-angle-right + ul + li.home: a(href='/') + i.icon.fa.fa-home + | ホーム + i.angle.fa.fa-angle-right + li.mentions: a(href='/i/mentions') + i.icon.fa.fa-at + | あなた宛て + i.angle.fa.fa-angle-right + li.notifications: a(href='/i/notifications') + i.icon.fa.fa-bell-o + | 通知 + i.angle.fa.fa-angle-right + li.messaging: a + i.icon.fa.fa-comments-o + | メッセージ + i.angle.fa.fa-angle-right + ul + li.settings: a(onclick={ search }) + i.icon.fa.fa-search + | 検索 + i.angle.fa.fa-angle-right + ul + li.settings: a(href='/i/drive') + i.icon.fa.fa-cloud + | ドライブ + i.angle.fa.fa-angle-right + li.settings: a(href='/i/upload') + i.icon.fa.fa-upload + | アップロード + i.angle.fa.fa-angle-right + ul + li.settings: a(href='/i/settings') + i.icon.fa.fa-cog + | 設定 + i.angle.fa.fa-angle-right + p.about + a Misskeyについて + +style. + display block + position fixed + top 0 + left 0 + z-index -1 + width 240px + color #fff + background #313538 + visibility hidden + + .body + height 100% + overflow hidden + + .content + min-height 100% + + .me + display block + margin 0 + padding 16px + + .avatar + display inline + max-width 64px + border-radius 32px + vertical-align middle + + .name + display block + margin 0 16px + position absolute + top 0 + left 80px + padding 0 + width calc(100% - 112px) + color #fff + line-height 96px + overflow hidden + text-overflow ellipsis + white-space nowrap + + ul + display block + margin 16px 0 + padding 0 + list-style none + + &:first-child + margin-top 0 + + li + display block + font-size 1em + line-height 1em + border-top solid 1px rgba(0, 0, 0, 0.2) + background #353A3E + background-clip content-box + + &:last-child + border-bottom solid 1px rgba(0, 0, 0, 0.2) + + a + display block + padding 0 20px + line-height 3rem + line-height calc(1rem + 30px) + color #eee + text-decoration none + + > .icon + margin-right 0.5em + + > .angle + position absolute + top 0 + right 0 + padding 0 20px + font-size 1.2em + line-height calc(1rem + 30px) + color #ccc + + > .unread-count + position absolute + height calc(0.9em + 10px) + line-height calc(0.9em + 10px) + top 0 + bottom 0 + right 38px + margin auto 0 + padding 0px 8px + min-width 2em + font-size 0.9em + text-align center + color #fff + background rgba(255, 255, 255, 0.1) + border-radius 1em + + .about + margin 1em 1em 2em 1em + text-align center + font-size 0.6em + opacity 0.3 + + a + color #fff + +script. + @mixin \i + @mixin \page + + @on \mount ~> + @opts.ready! + + @search = ~> + query = window.prompt \検索 + if query? and query != '' + @page '/search:' + query diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag new file mode 100644 index 0000000000..81dfac80ca --- /dev/null +++ b/src/web/app/mobile/tags/ui.tag @@ -0,0 +1,50 @@ +mk-ui + div.global@global + mk-ui-header@header(ready={ ready }) + mk-ui-nav@nav(ready={ ready }) + + div.content@main + <yield /> + + mk-stream-indicator + +style. + display block + + > .global + > .content + background #fff + +script. + @mixin \stream + + @ready-count = 0 + + #@ui.on \notification (text) ~> + # alert text + + @on \mount ~> + @stream.on \notification @on-stream-notification + @ready! + + @on \unmount ~> + @stream.off \notification @on-stream-notification + @slide.slide-close! + + @ready = ~> + @ready-count++ + + if @ready-count == 2 + @slide = SpSlidemenu @refs.main, @refs.nav.root, \#hamburger {direction: \left} + @init-view-position! + + @init-view-position = ~> + top = @refs.header.root.offset-height + @refs.main.style.padding-top = top + \px + @refs.nav.root.style.margin-top = top + \px + @refs.nav.root.query-selector '.body > .content' .style.padding-bottom = top + \px + + @on-stream-notification = (notification) ~> + el = document.body.append-child document.create-element \mk-notify + riot.mount el, do + notification: notification diff --git a/src/web/app/mobile/tags/user-followers.tag b/src/web/app/mobile/tags/user-followers.tag new file mode 100644 index 0000000000..7004398268 --- /dev/null +++ b/src/web/app/mobile/tags/user-followers.tag @@ -0,0 +1,22 @@ +mk-user-followers + mk-users-list@list(fetch={ fetch }, count={ user.followers_count }, you-know-count={ user.followers_you_know_count }, no-users={ 'フォロワーはいないようです。' }) + +style. + display block + +script. + @mixin \api + + @user = @opts.user + + @fetch = (iknow, limit, cursor, cb) ~> + @api \users/followers do + user_id: @user.id + iknow: iknow + limit: limit + cursor: if cursor? then cursor else undefined + .then cb + + @on \mount ~> + @refs.list.on \loaded ~> + @trigger \loaded diff --git a/src/web/app/mobile/tags/user-following.tag b/src/web/app/mobile/tags/user-following.tag new file mode 100644 index 0000000000..c122acd607 --- /dev/null +++ b/src/web/app/mobile/tags/user-following.tag @@ -0,0 +1,22 @@ +mk-user-following + mk-users-list@list(fetch={ fetch }, count={ user.following_count }, you-know-count={ user.following_you_know_count }, no-users={ 'フォロー中のユーザーはいないようです。' }) + +style. + display block + +script. + @mixin \api + + @user = @opts.user + + @fetch = (iknow, limit, cursor, cb) ~> + @api \users/following do + user_id: @user.id + iknow: iknow + limit: limit + cursor: if cursor? then cursor else undefined + .then cb + + @on \mount ~> + @refs.list.on \loaded ~> + @trigger \loaded diff --git a/src/web/app/mobile/tags/user-preview.tag b/src/web/app/mobile/tags/user-preview.tag new file mode 100644 index 0000000000..4f5fbc1520 --- /dev/null +++ b/src/web/app/mobile/tags/user-preview.tag @@ -0,0 +1,103 @@ +mk-user-preview + a.avatar-anchor(href={ CONFIG.url + '/' + user.username }) + img.avatar(src={ user.avatar_url + '?thumbnail&size=64' }, alt='avatar') + div.main + header + div.left + a.name(href={ CONFIG.url + '/' + user.username }) + | { user.name } + span.username + | @{ user.username } + div.body + div.bio { user.bio } + +style. + display block + margin 0 + padding 16px + font-size 12px + + @media (min-width 350px) + font-size 14px + + @media (min-width 500px) + font-size 16px + + &:after + content "" + display block + clear both + + > .avatar-anchor + display block + float left + margin 0 10px 0 0 + + @media (min-width 500px) + margin-right 16px + + > .avatar + display block + width 48px + height 48px + margin 0 + border-radius 6px + vertical-align bottom + + @media (min-width 500px) + width 58px + height 58px + border-radius 8px + + > .main + float left + width calc(100% - 58px) + + @media (min-width 500px) + width calc(100% - 74px) + + > header + white-space nowrap + + @media (min-width 500px) + margin-bottom 2px + + &:after + content "" + display block + clear both + + > .left + float left + + > .name + display inline + margin 0 + padding 0 + color #777 + font-size 1em + font-weight 700 + text-align left + text-decoration none + + &:hover + text-decoration underline + + > .username + text-align left + margin 0 0 0 8px + color #ccc + + > .body + + > .bio + cursor default + display block + margin 0 + padding 0 + word-wrap break-word + font-size 1.1em + color #717171 + +script. + @user = @opts.user diff --git a/src/web/app/mobile/tags/user-timeline.tag b/src/web/app/mobile/tags/user-timeline.tag new file mode 100644 index 0000000000..7aa23d2150 --- /dev/null +++ b/src/web/app/mobile/tags/user-timeline.tag @@ -0,0 +1,28 @@ +mk-user-timeline + mk-timeline(init={ init }, more={ more }, empty={ with-media ? 'メディア付き投稿はありません。' : 'このユーザーはまだ投稿していないようです。' }) + +style. + display block + max-width 600px + margin 0 auto + background #fff + +script. + @mixin \api + + @user = @opts.user + @with-media = @opts.with-media + + @init = new Promise (res, rej) ~> + @api \users/posts do + user_id: @user.id + with_media: @with-media + .then (posts) ~> + res posts + @trigger \loaded + + @more = ~> + @api \users/posts do + user_id: @user.id + with_media: @with-media + max_id: @refs.timeline.tail!.id diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag new file mode 100644 index 0000000000..8f4c04cf9c --- /dev/null +++ b/src/web/app/mobile/tags/user.tag @@ -0,0 +1,198 @@ +mk-user + div.user(if={ !fetching }) + header + div.banner(style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=1024)' : '' }) + div.body + div.top + a.avatar: img(src={ user.avatar_url + '?thumbnail&size=160' }, alt='avatar') + mk-follow-button(if={ SIGNIN && I.id != user.id }, user={ user }) + + div.title + h1 { user.name } + span.username @{ user.username } + span.followed(if={ user.is_followed }) フォローされています + + div.bio { user.bio } + + div.info + p.location(if={ user.location }) + i.fa.fa-map-marker + | { user.location } + + div.friends + a(href='{ user.username }/following') + b { user.following_count } + i フォロー + a(href='{ user.username }/followers') + b { user.followers_count } + i フォロワー + nav + a(data-is-active={ page == 'posts' }, onclick={ go-posts }) 投稿 + a(data-is-active={ page == 'media' }, onclick={ go-media }) メディア + a(data-is-active={ page == 'graphs' }, onclick={ go-graphs }) グラフ + a(data-is-active={ page == 'likes' }, onclick={ go-likes }) いいね + + div.body + mk-user-timeline(if={ page == 'posts' }, user={ user }) + mk-user-timeline(if={ page == 'media' }, user={ user }, with-media={ true }) + mk-user-graphs(if={ page == 'graphs' }, user={ user }) + +style. + display block + + > .user + > header + > .banner + padding-bottom 33.3% + background-color #f5f5f5 + background-size cover + background-position center + + > .body + padding 8px + margin 0 auto + max-width 600px + + > .top + &:after + content '' + display block + clear both + + > .avatar + display block + float left + width 25% + height 40px + + > img + display block + position absolute + left -2px + bottom -2px + width 100% + border 2px solid #fff + border-radius 6px + + @media (min-width 500px) + left -4px + bottom -4px + border 4px solid #fff + border-radius 12px + + > mk-follow-button + float right + height 40px + + > .title + margin 8px 0 + + > h1 + margin 0 + line-height 22px + font-size 20px + color #222 + + > .username + display inline-block + line-height 20px + font-size 16px + font-weight bold + color #657786 + + > .followed + margin-left 8px + padding 2px 4px + font-size 12px + color #657786 + background #f8f8f8 + border-radius 4px + + > .bio + margin 8px 0 + color #333 + + > .info + margin 8px 0 + + > .location + display inline + margin 0 + color #555 + + > i + margin-right 4px + + > .friends + > a + color #657786 + + &:first-child + margin-right 16px + + > b + margin-right 4px + font-size 16px + color #14171a + + > i + font-size 14px + + > nav + display flex + justify-content center + margin 0 auto + max-width 600px + border-bottom solid 1px #ddd + + > a + display block + flex 1 1 + text-align center + line-height 52px + font-size 14px + text-decoration none + color #657786 + border-bottom solid 2px transparent + + &[data-is-active] + font-weight bold + color $theme-color + border-color $theme-color + + > .body + @media (min-width 500px) + padding 16px 0 0 0 + +script. + @mixin \i + @mixin \api + + @username = @opts.user + @page = if @opts.page? then @opts.page else \posts + @fetching = true + + @on \mount ~> + @api \users/show do + username: @username + .then (user) ~> + @fetching = false + @user = user + @trigger \loaded user + @update! + + @go-posts = ~> + @page = \posts + @update! + + @go-media = ~> + @page = \media + @update! + + @go-graphs = ~> + @page = \graphs + @update! + + @go-likes = ~> + @page = \likes + @update! diff --git a/src/web/app/mobile/tags/users-list.tag b/src/web/app/mobile/tags/users-list.tag new file mode 100644 index 0000000000..3e29a0a4cc --- /dev/null +++ b/src/web/app/mobile/tags/users-list.tag @@ -0,0 +1,125 @@ +mk-users-list + nav + span(data-is-active={ mode == 'all' }, onclick={ set-mode.bind(this, 'all') }) + | すべて + span { opts.count } + // ↓ https://github.com/riot/riot/issues/2080 + span(if={ SIGNIN && opts.you-know-count != '' }, data-is-active={ mode == 'iknow' }, onclick={ set-mode.bind(this, 'iknow') }) + | 知り合い + span { opts.you-know-count } + + div.users(if={ !fetching && users.length != 0 }) + mk-user-preview(each={ users }, user={ this }) + + button.more(if={ !fetching && next != null }, onclick={ more }, disabled={ more-fetching }) + span(if={ !more-fetching }) もっと + span(if={ more-fetching }) + | 読み込み中 + mk-ellipsis + + p.no(if={ !fetching && users.length == 0 }) + | { opts.no-users } + p.fetching(if={ fetching }) + i.fa.fa-spinner.fa-pulse.fa-fw + | 読み込んでいます + mk-ellipsis + +style. + display block + background #fff + + > nav + display flex + justify-content center + margin 0 auto + max-width 600px + border-bottom solid 1px #ddd + + > span + display block + flex 1 1 + text-align center + line-height 52px + font-size 14px + color #657786 + border-bottom solid 2px transparent + + &[data-is-active] + font-weight bold + color $theme-color + border-color $theme-color + + > span + display inline-block + margin-left 4px + padding 2px 5px + font-size 12px + line-height 1 + color #888 + background #eee + border-radius 20px + + > .users + > * + max-width 600px + margin 0 auto + border-bottom solid 1px rgba(0, 0, 0, 0.05) + + > .no + margin 0 + padding 16px + text-align center + color #aaa + + > .fetching + margin 0 + padding 16px + text-align center + color #aaa + + > i + margin-right 4px + +script. + @mixin \i + + @limit = 30users + @mode = \all + + @fetching = true + @more-fetching = false + + @on \mount ~> + @fetch ~> + @trigger \loaded + + @fetch = (cb) ~> + @fetching = true + @update! + obj <~ @opts.fetch do + @mode == \iknow + @limit + null + @users = obj.users + @next = obj.next + @fetching = false + @update! + if cb? then cb! + + @more = ~> + @more-fetching = true + @update! + obj <~ @opts.fetch do + @mode == \iknow + @limit + @cursor + @users = @users.concat obj.users + @next = obj.next + @more-fetching = false + @update! + + @set-mode = (mode) ~> + @update do + mode: mode + + @fetch! |