Pārlūkot izejas kodu

added a channel picker and some hotkeys

tags/v0.8.0
Shaun pirms 4 gadiem
vecāks
revīzija
73f6c2f0b3
6 mainītis faili ar 490 papildinājumiem un 185 dzēšanām
  1. +4
    -4
      src/client/css/loader.less
  2. Binārs
     
  3. +291
    -56
      src/client/ui/templates/messages/messages.js
  4. +141
    -63
      src/client/ui/templates/messages/styles.less
  5. +11
    -5
      src/client/ui/templates/messages/template.html
  6. +43
    -57
      src/server/components/social/chat.js

+ 4
- 4
src/client/css/loader.less Parādīt failu

@@ -1,9 +1,8 @@
@import "colors.less";

@font-face
{
font-family: bitty;
src: url('../fonts/bitty.ttf');
@font-face {
font-family: bitty;
src: url('../fonts/bitty.ttf');
}

.loader-container {
@@ -99,4 +98,5 @@
}

}

}

Binārs
Parādīt failu


+ 291
- 56
src/client/ui/templates/messages/messages.js Parādīt failu

@@ -29,21 +29,35 @@ define([

hoverFilter: false,

lastChannel: null,
currentChannel: 'global',
currentSubChannel: null,

privateChannels: [],
lastPrivateChannel: null,
customChannels: [],
lastCustomChannel: null,

postRender: function () {
this.onEvent('onGetMessages', this.onGetMessages.bind(this));
this.onEvent('onDoWhisper', this.onDoWhisper.bind(this));
this.onEvent('onJoinChannel', this.onJoinChannel.bind(this));
this.onEvent('onLeaveChannel', this.onLeaveChannel.bind(this));
this.onEvent('onGetCustomChatChannels', this.onGetCustomChatChannels.bind(this));
this.onEvent('onToggleLastChannel', this.onToggleLastChannel.bind(this));

this
.find('.filter:not(.channel)')
[
'onGetMessages',
'onDoWhisper',
'onJoinChannel',
'onLeaveChannel',
'onClickFilter',
'onGetCustomChatChannels',
'onToggleLastChannel',
'onKeyDown',
'onKeyUp'
].forEach(e => this.onEvent(e, this[e].bind(this)));

//This whole hoverFilter business is a filthy hack
this.find('.channelPicker, .channelOptions, .filter:not(.channel)')
.on('mouseover', this.onFilterHover.bind(this, true))
.on('mouseleave', this.onFilterHover.bind(this, false))
.on('click', this.onClickFilter.bind(this));
.on('mouseleave', this.onFilterHover.bind(this, false));

this.find('.channelPicker').on('click', this.onShowChannelOptions.bind(this, null));

this.find('.filter:not(.channel)').on('click', this.onClickFilter.bind(this));

if (isMobile) {
this.kbUpper = 0;
@@ -62,9 +76,6 @@ define([
.on('input', this.checkChatLength.bind(this))
.on('blur', this.toggle.bind(this, false, true));
}

this.onEvent('onKeyDown', this.onKeyDown.bind(this));
this.onEvent('onKeyUp', this.onKeyUp.bind(this));
},

update: function () {
@@ -202,12 +213,12 @@ define([
},

onGetCustomChatChannels: function (channels) {
channels.forEach(function (c) {
this.onJoinChannel(c);
}, this);
channels.forEach(c => this.onJoinChannel(c));
},

onJoinChannel: function (channel) {
this.customChannels.push(channel);

this.find('[filter="' + channel.trim() + '"]').remove();

let container = this.find('.filters');
@@ -222,7 +233,9 @@ define([
},

onLeaveChannel: function (channel) {
this.find('.filters [filter="' + channel + '"]').remove();
this.customChannels.spliceWhere(c => c === channel);

this.find('.filters div[filter="' + channel + '"]').remove();
},

onFilterHover: function (hover) {
@@ -260,11 +273,37 @@ define([

onDoWhisper: function (charName) {
this.toggle(true);
let toName = charName;
if (charName.indexOf(' ') > -1)
toName = "'" + toName + "'";

this.find('input').val('@' + toName + ' ');
this.currentChannel = 'direct';
this.currentSubChannel = charName;

this.find('.channelPicker').html(charName);

const elInput = this.find('input')
.val('message')
.focus();

elInput[0].setSelectionRange(0, 7);
},

//Remember private and custom channels used
trackHistory: function (msg) {
const { subType, source, target, channel } = msg;

if (subType === 'privateIn' || subType === 'privateOut') {
const list = this.privateChannels;
list.spliceWhere(l => l === source);

//Newest sources are always at the end
list.push(source);

if (list.length > 5)
list.splice(0, list.length - 5);

if (subType === 'privateOut' && config.rememberChatChannel)
this.lastPrivateChannel = target;
} else if (subType === 'custom' && config.rememberChatChannel)
this.lastCustomChannel = channel;
},

onGetMessages: function (e) {
@@ -275,10 +314,14 @@ define([
let container = this.find('.list');

messages.forEach(m => {
this.trackHistory(m);

let message = m.message;

if (m.source && window.player.social.isPlayerBlocked(m.source))
return;
if (m.source) {
if (window.player.social.isPlayerBlocked(m.source))
return;
}

if (m.item) {
let source = message.split(':')[0];
@@ -362,7 +405,7 @@ define([
},

toggle: function (show, isFake, e) {
if ((isFake) && (this.hoverFilter))
if (isFake && this.hoverFilter)
return;

input.resetKeys();
@@ -373,16 +416,98 @@ define([

if (show) {
this.el.addClass('typing');

this.find('.channelPicker').html(this.currentSubChannel || this.currentChannel);
textbox.focus();
this.find('.list').scrollTop(9999999);
} else
} else {
this.find('.channelOptions').removeClass('active');
textbox.val('');
this.el.removeClass('picking');

if (['direct', 'custom'].includes(this.currentChannel) && (!this.currentSubChannel || ['join new', 'leave'].includes(this.currentSubChannel))) {
this.currentSubChannel = null;
this.currentChannel = 'global';
}
}

if (e)
e.stopPropagation();
},

processChat: function (msgConfig) {
const { message, event: keyboardEvent } = msgConfig;
const { key } = keyboardEvent;

if (message.length) {
if (this.el.hasClass('picking'))
msgConfig.cancel = true;
return;
}
if (key === 'Enter') {
const selectedSubPick = this.find('.channelOptions .option.selected');
if (selectedSubPick.length) {
this.onPickSubChannel(selectedSubPick.html(), this.currentChannel);
return;
}
}

//If we're busy picking a sub channel, we can use keyboard nav
const isPicking = this.el.hasClass('picking');
const optionContainer = this.find('.channelOptions');
const currentSelection = optionContainer.find('.option.selected');
if (isPicking && currentSelection.length) {
const delta = {
ArrowUp: -1,
ArrowDown: 1
}[key];

if (delta) {
const options = optionContainer.find('.option');
const currentIndex = currentSelection.eq(0).index();
let nextIndex = (currentIndex + delta) % options.length;
currentSelection.removeClass('selected');
options.eq(nextIndex).addClass('selected');
}
}

const pick = {
'%': 'party',
'!': 'global',
$: 'custom',
'@': 'direct'
}[key];

if (!pick) {
if (this.el.hasClass('picking'))
msgConfig.cancel = true;

return;
}

if (this.currentChannel === pick)
this.lastCustomChannel = null;

this.onPickChannel(pick, true);
msgConfig.cancel = true;
},

sendChat: function (e) {
let textbox = this.find('input');
let msgConfig = {
success: true,
message: textbox.val(),
event: e,
cancel: false
};

if (this.el.hasClass('picking')) {
this.processChat(msgConfig);
return;
}

if (e.which === 27) {
this.toggle(false);
return;
@@ -391,20 +516,19 @@ define([
let textfield = this.find('input');
textfield.val(`${textfield.val()} `);
return;
} else if (e.which !== 13)
return;
} else if (e.which !== 13) {
this.processChat(msgConfig);

const result = msgConfig.cancel === true ? false : null;

return result;
}

if (!this.el.hasClass('typing')) {
this.toggle(true);
return;
}

let textbox = this.find('input');
let msgConfig = {
success: true,
message: textbox.val()
};

events.emit('onBeforeChat', msgConfig);

let val = msgConfig.message
@@ -412,42 +536,153 @@ define([
.join('<')
.split('>')
.join('>');

textbox.blur();
if (!msgConfig.success)
return;

if (val.trim() === '')
return;

if (config.rememberChatChannel) {
const firstChar = val[0];
let lastChannel = null;
if ('@$'.includes(firstChar)) {
const firstSpace = val.indexOf(' ');
if (firstSpace === -1)
lastChannel = val + ' ';
else
lastChannel = val.substr(0, firstSpace) + ' ';
} else if (firstChar === '%')
lastChannel = '%';

this.lastChannel = lastChannel;
}

client.request({
cpn: 'social',
method: 'chat',
data: {
message: val
message: val,
type: this.currentChannel,
subType: this.currentSubChannel
}
});

this.toggle();
},

onToggleLastChannel: function (isOn) {
if (!isOn)
this.lastChannel = null;
if (isOn)
return;

this.lastPrivateChannel = null;
this.lastCustomChannel = null;
this.currentChannel = null;
this.currentSubChannel = null;
},

onPickChannel: function (channel, autoPickSub) {
this.currentChannel = channel;
this.currentSubChannel = null;

const showSubChannels = (
['direct', 'custom'].includes(channel) &&
(
!autoPickSub ||
(
channel === 'direct' &&
!this.lastPrivateChannel
) ||
(
channel === 'custom' &&
!this.lastCustomChannel
)
)
);

if (!showSubChannels) {
this.find('.channelOptions').removeClass('active');

let showValue = {
direct: this.lastPrivateChannel,
custom: this.lastCustomChannel
}[channel];

if (channel === 'direct' || channel === 'custom')
this.currentSubChannel = showValue;

showValue = showValue || channel;

this.find('.channelPicker').html(showValue);

this.find('input').focus();

this.el.removeClass('picking');
} else
this.onShowChannelOptions(channel);
},

onPickSubChannel: function (subChannel, channel) {
this.currentSubChannel = subChannel;
this.find('.channelOptions').removeClass('active');
this.find('.channelPicker').html(subChannel);

const elInput = this.find('input');

elInput.focus();

if (channel === 'custom') {
if (subChannel === 'join new') {
elInput.val('/join channelName');
elInput[0].setSelectionRange(6, 17);
} else if (subChannel === 'leave') {
elInput.val('/leave channelName');
elInput[0].setSelectionRange(7, 18);
}
}

this.el.removeClass('picking');
},

/* Todo
Don't allow party switch if not in a party
Arrow navigation fo different DMs and custom channels
Type name of private and custom channel
Typing @ while DMing someone should pop up the channels again
Mobile
*/

onShowChannelOptions: function (currentPick) {
const optionContainer = this.find('.channelOptions')
.addClass('active')
.empty();

const options = [];
let handlerOnClick = this.onPickChannel;

this.el.addClass('picking');

if (!currentPick) {
options.push('global', 'custom');

if (this.privateChannels.length)
options.push('direct');

//Hack...surely we can find a more sane way to do this
if ($('.uiParty .member').length)
options.push('party');
} else {
handlerOnClick = this.onPickSubChannel;
if (currentPick === 'direct')
options.push(...this.privateChannels);
else if (currentPick === 'custom')
options.push(...this.customChannels, 'join new', 'leave');
}
let addSelectStyleTo = null;
if (currentPick)
addSelectStyleTo = this.currentSubChannel || options[0];
options.forEach(o => {
const html = `<div class='option'>${o}</div>`;

const el = $(html)
.appendTo(optionContainer)
.on('click', handlerOnClick.bind(this, o, currentPick))
.on('hover', this.stopKeyboardNavForOptions.bind(this));

if (o === addSelectStyleTo)
el.addClass('selected');
});
},

stopKeyboardNavForOptions: function () {
this.find('.channelOptions .option.selected').removeClass('selected');
}
};
});

+ 141
- 63
src/client/ui/templates/messages/styles.less Parādīt failu

@@ -7,56 +7,14 @@
left: 10px;
bottom: 10px;
width: 480px;
padding: @pad;
pointer-events: none;

.input {
display: none;
}

&.typing {
pointer-events: all;

.el.textbox {
&.message:not(.input) {
display: block;
}

&.time {
display: none;
}

}

.filters {
pointer-events: all;
display: block;
}

.list {
overflow-y: auto;
background-color: @darkGray;

.list-message {
filter: none;
}

}

}

&.active {
.list-message {
opacity: 1 !important;
}

}
display: flex;
flex-direction: column;

.filters {
display: none;
width: 100%;
height: 26px;
margin-bottom: 10px;
flex-wrap: wrap;

.filter {
height: 100%;
@@ -64,6 +22,7 @@
float: left;
color: @blackA;
margin-right: 10px;
margin-bottom: 10px;
padding: 5px 10px;
cursor: pointer;

@@ -74,7 +33,7 @@

&.active {
background-color: @blackB;
color: @green;
color: @blueB;

&:hover {
background-color: @blackA;
@@ -169,19 +128,53 @@

}

.el {
&.textbox.message {
display: none;
background-color: @gray;
text-align: left;
padding: 5px 10px;
.bottom {
display: none;

.channelPicker {
min-width: 100px;
color: 1;
display: flex;
justify-content: center;
align-items: center;
color: @white;
background-color: @blueC;
cursor: pointer;
padding: 0px 10px;

&:after {
content: '▾';
margin-left: 10px;
}

&:hover {
background-color: @blueD;
}

}

.el {
flex: 1;
width: 100%;
color: @white;

&.textbox.message {
display: none;
background-color: @gray;
text-align: left;
padding: 5px 10px;
}

}

&.time {
display: block;
display: flex;
align-items: center;
padding-left: 10px;
background-color: transparent;
height: 35px;
color: @white;
text-align: left;
padding: 5px 10px;
filter: drop-shadow(0px -2px 0px @blackD)
drop-shadow(0px 2px 0px @blackD)
drop-shadow(2px 0px 0px @blackD)
@@ -192,8 +185,93 @@
drop-shadow(-2px 0px 0px @blackD);
}

width: 100%;
color: @white;
&.channelOptions {
display: none;
flex-direction: column;
position: absolute;
left: 0px;
bottom: 0px;
min-width: 100px;

.option {
height: 35px;
display: flex;
justify-content: center;
align-items: center;
color: @blueA;
background-color: @blackA;
cursor: pointer;
padding: 0px 10px;

&:hover,
&.selected {
background-color: @grayD;
}

}

}

}

&.typing {
pointer-events: all;

.filters {
pointer-events: all;
display: flex;
}

.list {
overflow-y: auto;
background-color: @darkGray;

.list-message {
filter: none;
}

}

.bottom {
display: flex;

.el.textbox {
&.message:not(.input) {
display: block;
}

}

}

.time {
display: none;
}

.channelOptions {
display: none;

&.active {
display: flex;
}

}

}

&.active {
.list-message {
opacity: 1 !important;
}

}

&.picking {
> *:not(.channelOptions) {
opacity: 0.5;
pointer-events: none;
}

}

}
@@ -232,7 +310,11 @@
background-color: fade(@darkGray, 90%);
display: flex;
flex-direction: column;
z-index: 999999999;
z-index: 999999998;

.channelOptions {
z-index: 999999999;
}

.input {
display: block;
@@ -240,12 +322,8 @@
flex-shrink: 0;
}

.el.textbox:not(.input) {
&.message,
&.time {
display: none;
}

.el.textbox.message:not(.input),.time {
display: none;
}

.keyboard {


+ 11
- 5
src/client/ui/templates/messages/template.html Parādīt failu

@@ -5,10 +5,16 @@
<div class="btn filter active" filter="chat">players</div>
<div class="btn filter active" filter="loot">loot</div>
</div>
<div class="list rep chat info loot">
<div class="list rep chat info loot"></div>
<div class="bottom">
<div class="channelPicker"></div>
<input type="text" class="el textbox message">
<div class="input el textbox message"></div>
</div>
<div class="bottom time"></div>
<div class="bottom channelOptions">
<div class="option">global</div>
<div class="option">party</div>
<div class="option">direct</div>
</div>
<input type="text" class="el textbox message">
<div class="input el textbox message"></div>
<div class="el textbox time"></div>
</div>

+ 43
- 57
src/server/components/social/chat.js Parādīt failu

@@ -39,7 +39,7 @@ const sendPartyMessage = ({ party, obj }, msg) => {
}

let charname = obj.auth.charname;
let message = msg.data.message.substr(1);
let message = msg.data.message;

party.forEach(p => {
let player = cons.players.find(c => c.id === p);
@@ -60,69 +60,53 @@ const sendPartyMessage = ({ party, obj }, msg) => {
const sendCustomChannelMessage = (cpnSocial, msg) => {
const { obj } = cpnSocial;

let pList = cons.players;
let pLen = pList.length;
let origMessage = msg.data.message.substr(1);
const { data: { message, subType: channel } } = msg;

let channel = origMessage.split(' ')[0];
let message = origMessage.substr(channel.length);

if ((!channel) || (!message)) {
obj.socket.emit('events', {
onGetMessages: [{
messages: [{
class: 'color-redA',
message: 'syntax: $channel message',
type: 'info'
}]
}]
});
if (!channel)
return;
} else if (!cpnSocial.isInChannel(obj, channel)) {

if (!cpnSocial.isInChannel(obj, channel)) {
obj.socket.emit('events', {
onGetMessages: [{
messages: [{
class: 'color-redA',
message: 'you are not currently in channel: ' + channel,
message: 'You are not currently in that channel',
type: 'info'
}]
}]
});
return;
} else if (pLen > 0) {
for (let i = 0; i < pLen; i++) {
if (cpnSocial.isInChannel(pList[i], channel)) {
pList[i].socket.emit('events', {
onGetMessages: [{
messages: [{
class: 'color-grayB',
message: '[' + channel + '] ' + obj.auth.charname + ': ' + message,
type: channel.trim(),
source: obj.name
}]
}]
});
}
}
}
};

const sendPrivateMessage = ({ obj: { name: sourcePlayerName, socket } }, msg) => {
let message = msg.data.message.substr(1);
const sendMessage = `[${channel}] ${obj.auth.charname}: ${message}`;
const eventData = {
onGetMessages: [{
messages: [{
class: 'color-grayB',
message: sendMessage,
type: 'chat',
subType: 'custom',
channel: channel.trim(),
source: obj.name
}]
}]
};

cons.players.forEach(p => {
if (!cpnSocial.isInChannel(p, channel))
return;

let playerName = '';
//Check if there's a space in the name
if (message[0] === "'")
playerName = message.substring(1, message.indexOf("'", 1));
else
playerName = message.substring(0, message.indexOf(' '));
p.socket.emit('events', eventData);
});
};

message = message.substr(playerName.length);
const sendPrivateMessage = ({ obj: { name: sourceName, socket } }, msg) => {
const { data: { message, subType: targetName } } = msg;

if (playerName === sourcePlayerName)
if (targetName === sourceName)
return;

let target = cons.players.find(p => p.name === playerName);
let target = cons.players.find(p => p.name === targetName);
if (!target)
return;

@@ -131,10 +115,10 @@ const sendPrivateMessage = ({ obj: { name: sourcePlayerName, socket } }, msg) =>
data: {
messages: [{
class: 'color-yellowB',
message: '(you to ' + playerName + '): ' + message,
message: '(you to ' + targetName + '): ' + message,
type: 'chat',
subType: 'privateOut',
source: sourcePlayerName
target: targetName
}]
}
});
@@ -144,10 +128,10 @@ const sendPrivateMessage = ({ obj: { name: sourcePlayerName, socket } }, msg) =>
data: {
messages: [{
class: 'color-yellowB',
message: '(' + sourcePlayerName + ' to you): ' + message,
message: '(' + sourceName + ' to you): ' + message,
type: 'chat',
subType: 'privateIn',
source: sourcePlayerName
source: sourceName
}]
}
});
@@ -196,7 +180,7 @@ module.exports = (cpnSocial, msg) => {
sendError('You have already sent that message');

return;
} else if (messageHistory.length >= 3) {
} else if (messageHistory.length >= 300) {
sendError('You are sending too many messages');

return;
@@ -230,13 +214,15 @@ module.exports = (cpnSocial, msg) => {
};
events.emit('onBeforeSendMessage', msgEvent);

const firstChar = messageString[0];

const messageHandler = {
$: sendCustomChannelMessage,
'@': sendPrivateMessage,
'%': sendPartyMessage
}[firstChar] || sendRegularMessage;
global: sendRegularMessage,
custom: sendCustomChannelMessage,
direct: sendPrivateMessage,
party: sendPartyMessage
}[msgData.type];

if (!messageHandler)
return;

messageHandler(cpnSocial, msg);
};

Notiek ielāde…
Atcelt
Saglabāt