yay emoji!!!
BIN
client/src/assets/images/emoji/neko.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.0 MiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
@ -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;
|
||||
|
1726
client/src/assets/styles/vendor/_emoji.scss
vendored
Normal file
5185
client/src/assets/styles/vendor/_emoji_20.scss
vendored
@ -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">
|
||||
|
@ -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>
|
||||
<neko-emoji v-if="emoji" @picked="onEmojiPicked" />
|
||||
<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 {
|
||||
|
@ -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">
|
||||
|
@ -6,39 +6,39 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="list" ref="scroll" @scroll="onScroll">
|
||||
<ul :class="[search === '' ? 'group-list' : 'emoji-list']">
|
||||
<template v-if="search === ''">
|
||||
<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"
|
||||
:key="`${group.id}-${emoji}`"
|
||||
:class="['emoji', hovered === emoji ? 'active' : '']"
|
||||
>
|
||||
<span
|
||||
:class="['emoji-20', `e-${emoji}`]"
|
||||
@mouseenter.stop.prevent="onMouseEnter($event, emoji)"
|
||||
@click.stop.prevent="onClick($event, emoji)"
|
||||
></span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
<template v-else>
|
||||
<li v-for="emoji in filtered" :key="emoji" :class="['emoji', hovered === emoji ? 'active' : '']">
|
||||
<span
|
||||
:class="['emoji-20', `e-${emoji}`]"
|
||||
@mouseenter.stop.prevent="onMouseEnter($event, emoji)"
|
||||
@click.stop.prevent="onClick($event, emoji)"
|
||||
></span>
|
||||
</li>
|
||||
</template>
|
||||
<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 index === 0 ? recent : group.list"
|
||||
:key="`${group.id}-${emoji}`"
|
||||
:class="['emoji-container', hovered === emoji ? 'active' : '']"
|
||||
>
|
||||
<span
|
||||
:class="['emoji']"
|
||||
@mouseenter.stop.prevent="onMouseEnter($event, emoji)"
|
||||
@click.stop.prevent="onClick($event, emoji)"
|
||||
:data-emoji="emoji"
|
||||
></span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</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']"
|
||||
@mouseenter.stop.prevent="onMouseEnter($event, emoji)"
|
||||
@click.stop.prevent="onClick($event, emoji)"
|
||||
:data-emoji="emoji"
|
||||
></span>
|
||||
</li>
|
||||
</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)
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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),
|
||||
|
@ -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">
|
||||
|
@ -26,5 +26,6 @@ new Vue({
|
||||
render: h => h(app),
|
||||
created() {
|
||||
this.$client.init(this)
|
||||
this.$accessor.initialise()
|
||||
},
|
||||
}).$mount('#neko')
|
||||
|
@ -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
@ -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)
|
||||
},
|
||||
},
|
||||
)
|
@ -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)
|
||||
|
@ -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
|
||||
|