Initial commit 🍀

This commit is contained in:
syuilo
2016-12-29 07:49:51 +09:00
commit b3f42e62af
405 changed files with 31017 additions and 0 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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);
});

View File

@@ -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);

View File

@@ -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

View File

@@ -0,0 +1,6 @@
riot = require \riot
ui = riot.observable!
riot.mixin \ui do
ui: ui

View File

@@ -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

View File

@@ -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'

View File

@@ -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!

View File

@@ -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

View File

@@ -0,0 +1,8 @@
mk-drive-file-viewer
p.name { file.name }
style.
display block
script.
@file = @opts.file

View File

@@ -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

View File

@@ -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

View File

@@ -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!

View File

@@ -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!

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 + '日'

View File

@@ -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

View File

@@ -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

View File

@@ -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!

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -0,0 +1,5 @@
mk-new-post-page
mk-post-form@form
style.
display block

View File

@@ -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!

View File

@@ -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!

View File

@@ -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!

View File

@@ -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!

View File

@@ -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!

View File

@@ -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

View File

@@ -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!

View File

@@ -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!

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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!

View File

@@ -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]

View File

@@ -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!

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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!

View File

@@ -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!