yay emoji!!!

This commit is contained in:
Craig 2020-02-01 20:35:48 +00:00
parent 1f1e67b829
commit 98980cc565
36 changed files with 2211 additions and 11779 deletions

View File

@ -15,6 +15,7 @@
"scripts": {
"serve": "vue-cli-service serve --mode development",
"build": "vue-cli-service build",
"build:emoji": "ts-node --files --project tools/tsconfig.json tools/emoji.ts",
"lint": "vue-cli-service lint"
},
"dependencies": {
@ -22,6 +23,8 @@
"animejs": "^3.1.0",
"axios": "^0.19.1",
"date-fns": "^2.9.0",
"emoji-datasource": "^5.0.1",
"emojilib": "^2.4.0",
"eventemitter3": "^4.0.0",
"resize-observer-polyfill": "^1.5.1",
"simple-markdown": "^0.7.2",
@ -37,6 +40,7 @@
},
"devDependencies": {
"@types/animejs": "^3.1.0",
"@types/node": "^13.7.0",
"@types/vue": "^2.0.0",
"@vue/cli-plugin-babel": "^4.1.0",
"@vue/cli-plugin-eslint": "^4.1.0",
@ -51,6 +55,7 @@
"node-sass": "^4.12.0",
"prettier": "^1.19.1",
"sass-loader": "^8.0.0",
"ts-node": "^8.6.2",
"typescript": "~3.5.3",
"vue-template-compiler": "^2.6.10"
}

1
client/public/emoji.json Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -12,7 +12,7 @@
@import "vendor/swal";
@import "vendor/tooltip";
@import "vendor/github";
@import "vendor/emoji_20";
@import "vendor/emoji";
html, body {
-webkit-font-smoothing: subpixel-antialiased;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
<div class="window">
<div class="loading" v-if="loading">
<div class="logo">
<img src="@/assets/logo.svg" alt="n.eko" />
<img src="@/assets/images/logo.svg" alt="n.eko" />
<span><b>N</b>.EKO</span>
</div>
<div class="loader">

View File

@ -42,9 +42,8 @@
v-model="content"
@click.stop.prevent="emoji = false"
/>
<div class="emoji" @click.stop.prevent="emoji = !emoji">
<neko-emoji v-if="emoji" @picked="onEmojiPicked" />
</div>
<i class="emoji-menu fas fa-laugh" @click.stop.prevent="emoji = !emoji"></i>
</div>
</div>
</div>
@ -101,6 +100,8 @@
word-wrap: break-word;
&.message {
font-size: 16px;
.author {
flex-grow: 0;
flex-shrink: 0;
@ -134,7 +135,6 @@
display: inline-block;
color: $text-normal;
font-weight: 500;
font-size: 16px;
}
.timestamp {
@ -152,7 +152,7 @@
::v-deep .content-body {
color: $text-normal;
line-height: 20px;
line-height: 22px;
word-wrap: break-word;
overflow-wrap: break-word;
@ -273,14 +273,15 @@
height: 100%;
background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px;
position: relative;
display: flex;
.emoji {
.emoji-menu {
width: 20px;
height: 20px;
// background: #fff;
margin: 3px 3px 0 0;
position: relative;
font-size: 20px;
margin: 8px 5px 0 0;
cursor: pointer;
}
textarea {

View File

@ -2,7 +2,7 @@
<div class="connect">
<div class="window">
<div class="logo">
<img src="@/assets/logo.svg" alt="n.eko" />
<img src="@/assets/images/logo.svg" alt="n.eko" />
<span><b>n</b>.eko</span>
</div>
<form class="message" v-if="!connecting" @submit.stop.prevent="connect">

View File

@ -6,39 +6,39 @@
</div>
</div>
<div class="list" ref="scroll" @scroll="onScroll">
<ul :class="[search === '' ? 'group-list' : 'emoji-list']">
<template v-if="search === ''">
<ul :class="['group-list']" :style="{ display: search === '' ? 'flex' : 'none' }">
<li v-for="(group, index) in groups" :key="index" class="group" ref="groups">
<span class="label">{{ group.name }}</span>
<ul class="emoji-list">
<li
v-for="emoji in group.list"
v-for="emoji in index === 0 ? recent : group.list"
:key="`${group.id}-${emoji}`"
:class="['emoji', hovered === emoji ? 'active' : '']"
:class="['emoji-container', hovered === emoji ? 'active' : '']"
>
<span
:class="['emoji-20', `e-${emoji}`]"
:class="['emoji']"
@mouseenter.stop.prevent="onMouseEnter($event, emoji)"
@click.stop.prevent="onClick($event, emoji)"
:data-emoji="emoji"
></span>
</li>
</ul>
</li>
</template>
<template v-else>
<li v-for="emoji in filtered" :key="emoji" :class="['emoji', hovered === emoji ? 'active' : '']">
</ul>
<ul :class="['emoji-container']" :style="{ display: search === '' ? 'none' : 'flex' }">
<li v-for="emoji in filtered" :key="emoji" :class="['emoji-item', hovered === emoji ? 'active' : '']">
<span
:class="['emoji-20', `e-${emoji}`]"
:class="['emoji']"
@mouseenter.stop.prevent="onMouseEnter($event, emoji)"
@click.stop.prevent="onClick($event, emoji)"
:data-emoji="emoji"
></span>
</li>
</template>
</ul>
</div>
<div class="details">
<div class="icon-container" v-if="hovered !== ''">
<span :class="['icon', 'emoji-20', `e-${hovered}`]"></span><span class="emoji">:{{ hovered }}:</span>
<div class="details-container" v-if="hovered !== ''">
<span :class="['emoji']" :data-emoji="hovered" /><span class="emoji-id">:{{ hovered }}:</span>
</div>
</div>
<div class="groups">
@ -46,10 +46,10 @@
<li
v-for="(group, index) in groups"
:key="index"
:class="[group.id, active === group.id && search === '' ? 'active' : '']"
:class="[group.id, active.id === group.id && search === '' ? 'active' : '']"
@click.stop.prevent="scrollTo($event, index)"
>
<span :class="[`group-${group.id}`]" />
<span :class="[`group-${group.id} fas`]" />
</li>
</ul>
</div>
@ -65,8 +65,8 @@
width: $emoji-width;
height: 350px;
background: $background-secondary;
bottom: 30px;
right: 0;
bottom: 75px;
right: 5px;
display: flex;
flex-direction: column;
border-radius: 5px;
@ -169,7 +169,7 @@
flex-direction: row;
flex-wrap: wrap;
li {
&.emoji {
&.emoji-container {
padding: 2px;
border-radius: 3px;
cursor: pointer;
@ -191,7 +191,7 @@
height: 36px;
background: $background-tertiary;
.icon-container {
.details-container {
display: flex;
align-content: center;
flex-direction: row;
@ -200,11 +200,11 @@
span {
cursor: default;
&.icon {
&.emoji {
margin: 0 5px 0 10px;
}
&.emoji {
&.emoji-id {
line-height: 20px;
font-size: 16px;
font-weight: 500;
@ -242,36 +242,42 @@
margin: 0 auto;
height: 20px;
width: 20px;
font-size: 16px;
line-height: 20px;
text-align: center;
&.group-recent {
background-color: #fff;
&.group-recent::before {
content: '\f017';
}
&.group-neko {
background-color: #fff;
&.group-neko::before {
content: '\f6be';
}
&.group-people {
background-color: #fff;
&.group-emotion::before {
content: '\f118';
}
&.group-nature {
background-color: #fff;
&.group-people::before {
content: '\f0c0';
}
&.group-food {
background-color: #fff;
&.group-nature::before {
content: '\f1b0';
}
&.group-activity {
background-color: #fff;
&.group-food::before {
content: '\f5d1';
}
&.group-travel {
background-color: #fff;
&.group-activity::before {
content: '\f44e';
}
&.group-objects {
background-color: #fff;
&.group-travel::before {
content: '\f1b9';
}
&.group-symbols {
background-color: #fff;
&.group-objects::before {
content: '\f0eb';
}
&.group-flags {
background-color: #fff;
&.group-symbols::before {
content: '\f86d';
}
&.group-flags::before {
content: '\f024';
}
}
}
@ -283,9 +289,7 @@
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { list } from './emoji/list'
import { keywords } from './emoji/keywords'
import { groups } from './emoji/groups'
import { get, set } from '../utils/localstorage'
@Component({
name: 'neko-emoji',
@ -297,19 +301,32 @@
waitingForPaint = false
search = ''
active = groups[0].id
index = 0
hovered = ''
recent: string[] = JSON.parse(get('emoji_recent', '[]'))
get active() {
return this.$accessor.emoji.groups[this.index]
}
get keywords() {
return this.$accessor.emoji.keywords
}
get groups() {
return groups
return this.$accessor.emoji.groups
}
get list() {
return this.$accessor.emoji.list
}
get filtered() {
const filtered = []
for (const emoji of list) {
for (const emoji of this.list) {
if (
emoji.includes(this.search) || typeof keywords[emoji] !== 'undefined'
? keywords[emoji].some(keyword => keyword.includes(this.search))
emoji.includes(this.search) || typeof this.keywords[emoji] !== 'undefined'
? this.keywords[emoji].some(keyword => keyword.includes(this.search))
: false
) {
filtered.push(emoji)
@ -319,16 +336,10 @@
}
scrollTo(event: MouseEvent, index: number) {
const ele = this._groups[index]
if (!ele) {
if (!this._groups[index]) {
return
}
let top = ele.offsetTop
if (index == 0) {
top = 0
}
this._scroll.scrollTop = top
this._scroll.scrollTop = index == 0 ? 0 : this._groups[index].offsetTop
}
onScroll() {
@ -341,16 +352,17 @@
onScrollPaint() {
this.waitingForPaint = false
let scrollTop = this._scroll.scrollTop
let active = this.groups[0]
for (let i = 0, l = this.groups.length; i < l; i++) {
let group = this.groups[i]
let active = 0
for (const [i, group] of this.groups.entries()) {
let component = this._groups[i]
if (component && component.offsetTop > scrollTop) {
break
}
active = group
active = i
}
if (this.index !== active) {
this.index = active
}
this.active = active.id
}
onMouseExit(event: MouseEvent, emoji: string) {
@ -363,6 +375,7 @@
}
onClick(event: MouseEvent, emoji: string) {
this.$accessor.emoji.setRecent(emoji)
this.$emit('picked', emoji)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -29,27 +29,27 @@
background-size: contain;
&.celebrate {
background-image: url('../assets/celebrate.png');
background-image: url('../assets/images/emote/celebrate.png');
}
&.clap {
background-image: url('../assets/clap.png');
background-image: url('../assets/images/emote/clap.png');
}
&.exclam {
background-image: url('../assets/exclam.png');
background-image: url('../assets/images/emote/exclam.png');
}
&.heart {
background-image: url('../assets/heart.png');
background-image: url('../assets/images/emote/heart.png');
}
&.laughing {
background-image: url('../assets/laughing.png');
background-image: url('../assets/images/emote/laughing.png');
}
&.sleep {
background-image: url('../assets/sleep.png');
background-image: url('../assets/images/emote/sleep.png');
}
}
}

View File

@ -30,27 +30,27 @@
background-size: contain;
&.celebrate {
background-image: url('../assets/celebrate.png');
background-image: url('../assets/images/emote/celebrate.png');
}
&.clap {
background-image: url('../assets/clap.png');
background-image: url('../assets/images/emote/clap.png');
}
&.exclam {
background-image: url('../assets/exclam.png');
background-image: url('../assets/images/emote/exclam.png');
}
&.heart {
background-image: url('../assets/heart.png');
background-image: url('../assets/images/emote/heart.png');
}
&.laughing {
background-image: url('../assets/laughing.png');
background-image: url('../assets/images/emote/laughing.png');
}
&.sleep {
background-image: url('../assets/sleep.png');
background-image: url('../assets/images/emote/sleep.png');
}
}
}

View File

@ -1,7 +1,7 @@
<template>
<div class="header">
<div class="neko">
<img src="@/assets/logo.svg" alt="n.eko" />
<img src="@/assets/images/logo.svg" alt="n.eko" />
<span><b>n</b>.eko</span>
</div>
<ul class="menu">

View File

@ -212,6 +212,18 @@ const rules: MarkdownRules = {
...br,
match: md.anyScopeRegex(/^\n/),
},
emoji: {
order: md.defaultRules.strong.order,
match: source => /^:([a-zA-z_-]*):/.exec(source),
parse(capture) {
return {
id: capture[1],
}
},
html(node, output, state) {
return htmlTag('span', '', { class: `emoji`, 'data-emoji': node.id }, state)
},
},
emoticon: {
order: md.defaultRules.text.order,
match: source => /^(¯\\_\(ツ\)_\/¯)/.exec(source),

View File

@ -2,7 +2,7 @@
<div class="unsupported">
<div class="window">
<div class="logo">
<img src="@/assets/logo.svg" alt="n.eko" />
<img src="@/assets/images/logo.svg" alt="n.eko" />
<span><b>n</b>.eko</span>
</div>
<div class="message">

View File

@ -26,5 +26,6 @@ new Vue({
render: h => h(app),
created() {
this.$client.init(this)
this.$accessor.initialise()
},
}).$mount('#neko')

View File

@ -1,6 +1,14 @@
import { PluginObject } from 'vue'
import axios, { AxiosStatic } from 'axios'
declare global {
const $http: AxiosStatic
interface Window {
$http: AxiosStatic
}
}
declare module 'vue/types/vue' {
interface Vue {
$http: AxiosStatic
@ -9,7 +17,8 @@ declare module 'vue/types/vue' {
const plugin: PluginObject<undefined> = {
install(Vue) {
Vue.prototype.$http = axios
window.$http = axios
Vue.prototype.$http = window.$http
},
}

74
client/src/store/emoji.ts Normal file
View File

@ -0,0 +1,74 @@
import { getterTree, mutationTree, actionTree } from 'typed-vuex'
import { get, set } from '~/utils/localstorage'
import { accessor } from '~/store'
export const namespaced = true
interface Group {
name: string
id: string
list: string[]
}
interface Keywords {
[name: string]: string[]
}
interface Emojis {
groups: Group[]
keywords: Keywords
list: string[]
}
export const state = () => ({
groups: [
{
id: 'recent',
name: 'Recent',
list: JSON.parse(get('emoji_recent', '[]')) as string[],
},
] as Group[],
keywords: {} as Keywords,
list: [] as string[],
})
export const getters = getterTree(state, {})
export const mutations = mutationTree(state, {
setRecent(state, emoji: string) {
if (!state.groups[0].list.includes(emoji)) {
if (state.groups[0].list.length > 30) {
state.groups[0].list.shift()
}
state.groups[0].list.push(emoji)
set('emoji_recent', JSON.stringify(state.groups[0].list))
}
},
addGroup(state, group: Group) {
state.groups.push(group)
},
setKeywords(state, keywords: Keywords) {
state.keywords = keywords
},
setList(state, list: string[]) {
state.list = list
},
})
export const actions = actionTree(
{ state, getters, mutations },
{
initialise() {
$http
.get<Emojis>('/emoji.json')
.then(req => {
for (const group of req.data.groups) {
accessor.emoji.addGroup(group)
}
accessor.emoji.setList(req.data.list)
accessor.emoji.setKeywords(req.data.keywords)
})
.catch(console.error)
},
},
)

View File

@ -8,6 +8,7 @@ import * as remote from './remote'
import * as user from './user'
import * as settings from './settings'
import * as client from './client'
import * as emoji from './emoji'
export const state = () => ({
connecting: false,
@ -34,6 +35,11 @@ export const mutations = mutationTree(state, {
export const actions = actionTree(
{ state, mutations },
{
//
initialise(store) {
accessor.emoji.initialise()
},
//
connect(store, { username, password }: { username: string; password: string }) {
$client.connect(password, username)
@ -45,7 +51,7 @@ export const storePattern = {
state,
mutations,
actions,
modules: { video, chat, user, remote, settings, client },
modules: { video, chat, user, remote, settings, client, emoji },
}
Vue.use(Vuex)

View File

@ -44,10 +44,6 @@ export const mutations = mutationTree(state, {
export const actions = actionTree(
{ state, getters, mutations },
{
initialise({ commit }) {
//
},
sendClipboard({ getters }, clipboard: string) {
if (!accessor.connected || !getters.hosting) {
return

234
client/tools/emoji.ts Normal file
View File

@ -0,0 +1,234 @@
import * as fs from 'fs'
import { custom } from './emoji_custom'
const datasource = require('emoji-datasource/emoji.json') as EmojiDatasource[]
const emojis = require('emojilib/emojis.json') as { [id: string]: Emoji }
interface EmojiDatasource {
name: string
unified: string
non_qualified: string | null
docomo: string | null
au: string | null
softbank: string | null
google: string | null
image: string
sheet_x: number
sheet_y: number
short_name: string
short_names: string[]
text: string | null
texts: string | null
category: string
sort_order: number
added_in: string
has_img_apple: boolean
has_img_google: boolean
has_img_twitter: boolean
has_img_facebook: boolean
skin_variations: {
[id: string]: {
unified: string
image: string
sheet_x: number
sheet_y: number
added_in: string
has_img_apple: boolean
has_img_google: boolean
has_img_twitter: boolean
has_img_facebook: boolean
}
}
obsoletes: string
obsoleted_by: string
}
interface Emoji {
keywords: string[]
char: string
fitzpatrick_scale: boolean
category: string
}
const SHEET_COLUMNS = 57
const MULTIPLY = 100 / (SHEET_COLUMNS - 1)
const css: string[] = []
const keywords: { [name: string]: string[] } = {}
const list: string[] = []
const groups: { [name: string]: string[] } = { neko: [] }
for (const emoji of custom) {
groups['neko'].push(emoji.name)
list.push(emoji.name)
keywords[emoji.name] = emoji.keywords
// prettier-ignore
css.push(`&[data-emoji='${emoji.name}'] { background-size: contain; background-image: url('../images/emoji/${emoji.file}'); }`)
}
for (const source of datasource) {
const unified = source.unified.split('-').map(v => v.toLowerCase())
let emoji: Emoji | null = null
let emoji_id: string = ''
for (const id of Object.keys(emojis)) {
if (unified.includes(emojis[id].char.codePointAt(0)!.toString(16))) {
emoji_id = id
emoji = emojis[id]
break
}
}
if (!source.has_img_twitter) {
console.log(source.short_name, 'not avalible for set twitter')
continue
}
// keywords
let words: string[] = []
if (!emoji) {
console.log(source.short_name, 'no keywords')
} else {
words = [emoji_id, ...emoji.keywords]
}
for (const name of source.short_names) {
if (!words.includes(name)) {
words.push(name)
}
}
keywords[source.short_name] = words
// keywords
let group = ''
switch (source.category) {
case 'Symbols':
group = 'symbols'
break
case 'Activities':
group = 'activity'
break
case 'Flags':
group = 'flags'
break
case 'Travel & Places':
group = 'travel'
break
case 'Food & Drink':
group = 'food'
break
case 'Animals & Nature':
group = 'nature'
break
case 'People & Body':
group = 'people'
break
case 'Smileys & Emotion':
group = 'emotion'
break
case 'Objects':
group = 'objects'
break
case 'Skin Tones':
continue
default:
console.log(`unknown category ${source.category}`)
continue
}
if (!groups[group]) {
groups[group] = [source.short_name]
} else {
groups[group].push(source.short_name)
}
// list
list.push(source.short_name)
// css
// prettier-ignore
css.push(`&[data-emoji='${source.short_name}'] { background-position: ${MULTIPLY * source.sheet_x}% ${MULTIPLY * source.sheet_y}% }`)
}
fs.writeFile(
'src/assets/styles/vendor/_emoji.scss',
`
.emoji {
display: inline-block;
background-size: ${SHEET_COLUMNS * 100}%;
background-image: url('~emoji-datasource/img/twitter/sheets/32.png');
background-repeat: no-repeat;
vertical-align: bottom;
height: 22px;
width: 22px;
${css.map(v => ` ${v}`).join('\n')}
}
`,
() => {
console.log('_emoji.scss done')
},
)
const data = {
groups: [
{
id: 'neko',
name: 'Neko',
list: groups['neko'] ? groups['neko'] : [],
},
{
id: 'emotion',
name: 'Emotion',
list: groups['emotion'] ? groups['emotion'] : [],
},
{
id: 'people',
name: 'People',
list: groups['people'] ? groups['people'] : [],
},
{
id: 'nature',
name: 'Nature',
list: groups['nature'] ? groups['nature'] : [],
},
{
id: 'food',
name: 'Food',
list: groups['food'] ? groups['food'] : [],
},
{
id: 'activity',
name: 'Activity',
list: groups['activity'] ? groups['activity'] : [],
},
{
id: 'travel',
name: 'Travel',
list: groups['travel'] ? groups['travel'] : [],
},
{
id: 'objects',
name: 'Objects',
list: groups['objects'] ? groups['objects'] : [],
},
{
id: 'symbols',
name: 'Symbols',
list: groups['symbols'] ? groups['symbols'] : [],
},
{
id: 'flags',
name: 'Flags',
list: groups['flags'] ? groups['flags'] : [],
},
],
list,
keywords,
}
fs.writeFile('public/emoji.json', JSON.stringify(data), () => {
console.log('emoji.json done')
})

View File

@ -0,0 +1,7 @@
export const custom = [
{
name: 'neko',
file: 'neko.png',
keywords: ['neko', 'cat', 'cat butt', 'butt'],
},
]

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"moduleResolution": "node",
"types": [
"node"
],
},
"include": [
"**/*.ts"
]
}

View File

@ -13,6 +13,7 @@
"skipLibCheck": true,
"baseUrl": ".",
"types": [
"node",
"webpack-env"
],
"paths": {
@ -31,6 +32,7 @@
],
},
"include": [
"tools/**/*.ts",
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",