Add channel support

This commit is contained in:
Trevor Slocum 2019-12-19 15:58:44 -08:00
parent d2aecadbc4
commit 32c12d6250
11 changed files with 627 additions and 230 deletions

View File

@ -1,5 +1,6 @@
0.1.2:
- Add channel support
- Improve interface on mobile devices
- Notify user when browser does not support WebRTC
0.1.1:

View File

@ -20,9 +20,9 @@ or [Apache](https://httpd.apache.org/) via
### Caddy example
Launch harmony-server with the following flag:
Launch harmony-server with the following configuration option:
```-web-address=localhost:19000```
```webaddress: localhost:19000```
Add the following to the relevant site directive in your Caddyfile:
@ -58,10 +58,39 @@ Execute harmony-server with the ```-help``` flag for more information:
If the command was not found, add ```~/go/bin``` to your
[PATH variable](https://en.wikipedia.org/wiki/PATH_%28variable%29).
# Configuration
The path to a configuration file may be specified with ```--config```, or the
default path of ```~/.config/harmony-server/config.yaml``` may be used.
Example configuration:
```
webaddress: localhost:19000
webpath: /
channels:
Lobby-Text:
type: text
topic: Example text channel
Lobby-Voice:
type: voice
topic: Example voice channel
Lobby-Voice2:
type: voice
topic: Another voice channel
```
## Hosting the web client on a different path
The web client expects to be hosted at the root of the domain, ```/```.
To host the web client on a different path, use the ```-web-path``` flag:
To host the web client on a different path, such as ```/chat/```, set the
```webpath``` configuration option:
```harmony-server -web-path=/chat/```
```webpath: /chat/```
## Hosting the web client on multiple addresses
Multiple address may be specified:
```webaddress: localhost:19000,192.168.0.101:19010```

View File

@ -0,0 +1,77 @@
package main
import (
"io/ioutil"
"os"
"strings"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
type ChannelConfig struct {
Type string
Topic string
}
type Config struct {
WebAddress string
WebPath string
Channels map[string]ChannelConfig
}
var config = Config{WebPath: "/"}
func readconfig(configPath string) error {
if configPath == "" {
return errors.New("file unspecified")
}
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return errors.New("file not found")
}
configData, err := ioutil.ReadFile(configPath)
if err != nil {
return err
}
err = yaml.Unmarshal(configData, &config)
if err != nil {
return err
}
err = validateConfig()
if err != nil {
return err
}
if len(config.Channels) == 0 {
addPlaceholderchannels()
}
newChannels := make(map[string]ChannelConfig)
for channelName, ch := range config.Channels {
newChannels[strings.TrimSpace(channelName)] = ChannelConfig{Type: strings.TrimSpace(ch.Type), Topic: strings.TrimSpace(ch.Topic)}
}
config.Channels = newChannels
return nil
}
func validateConfig() error {
if config.Channels == nil {
config.Channels = make(map[string]ChannelConfig)
}
if config.WebAddress == "" {
return errors.New("WebAddress is required")
}
return nil
}
func addPlaceholderchannels() {
config.Channels["Lobby-Text"] = ChannelConfig{Type: "text"}
config.Channels["Lobby-Voice"] = ChannelConfig{Type: "voice"}
}

View File

@ -5,36 +5,55 @@ import (
"log"
"net/http"
_ "net/http/pprof"
"os"
"path"
"strings"
"git.sr.ht/~tslocum/harmony/pkg/agent"
"git.sr.ht/~tslocum/harmony/pkg/web"
)
var (
configPath string
debugAddress string
webAddress string
webPath string
)
func main() {
flag.StringVar(&configPath, "config", "", "path to configuration file")
flag.StringVar(&debugAddress, "debug-address", "", "address to serve debug info")
flag.StringVar(&webAddress, "web-address", "", "address to serve web client")
flag.StringVar(&webPath, "web-path", "/", "path to serve web client")
flag.Parse()
if webAddress == "" {
log.Fatal("Argument -web-address is required")
}
if debugAddress != "" {
go func() {
log.Fatal(http.ListenAndServe(debugAddress, nil))
}()
}
w := web.NewWebInterface(webAddress, webPath)
_ = w
if configPath == "" {
homedir, err := os.UserHomeDir()
if err == nil && homedir != "" {
configPath = path.Join(homedir, ".config", "harmony-server", "config.yaml")
}
}
err := readconfig(configPath)
if err != nil {
log.Fatalf("Failed to read configuration file %s: %v\nSee HOSTING.md for information on how to configure harmony-server", configPath, err)
}
w := web.NewWebInterface(config.WebAddress, config.WebPath)
for channelName, ch := range config.Channels {
t := agent.ChannelText
if ch.Type != "" && strings.ToLower(ch.Type)[0] == 'v' {
t = agent.ChannelVoice
}
w.AddChannel(t, channelName, ch.Topic)
}
log.Println("harmony-server started")
_ = w
select {}
}

2
go.mod
View File

@ -15,5 +15,5 @@ require (
github.com/pkg/errors v0.8.1
gitlab.com/golang-commonmark/markdown v0.0.0-20191127184510-91b5b3c99c19
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect
gopkg.in/yaml.v2 v2.2.4 // indirect
gopkg.in/yaml.v2 v2.2.7
)

2
go.sum
View File

@ -146,3 +146,5 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,6 +1,7 @@
package agent
import (
"strings"
"sync"
)
@ -8,9 +9,8 @@ type ChannelType int
const (
ChannelUnknown ChannelType = 0
ChannelAll ChannelType = 1
ChannelText ChannelType = 2
ChannelVoice ChannelType = 3
ChannelText ChannelType = 1
ChannelVoice ChannelType = 2
)
type Channel struct {
@ -36,4 +36,14 @@ type ChannelListing struct {
Topic string
}
type ChannelList map[int]*ChannelListing
type ChannelList []*ChannelListing
func (c ChannelList) Len() int { return len(c) }
func (c ChannelList) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c ChannelList) Less(i, j int) bool {
if c[i] == nil || c[j] == nil {
return c[j] != nil
}
return c[i].Type < c[j].Type || (c[i].Type == c[j].Type && strings.ToLower(c[i].Name) < strings.ToLower(c[j].Name))
}

View File

@ -6,8 +6,8 @@
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
width: 100vw;
height: 100vh;
}
nav ul, aside ul {
@ -16,11 +16,104 @@ nav ul, aside ul {
margin: 0 0 0 5px;
}
.content {
grid-area: content;
button {
border: 1px solid black;
padding: 7px;
}
.buffer {
grid-area: buffer;
}
.mobileside {
grid-area: mobileside;
overflow-x: auto;
overflow-y: scroll;
}
.sidemenu > li {
margin-bottom: 7px;
}
.menulink {
display: inline-block;
width: 100%;
cursor: pointer;
}
.menulinkfloat {
display: inline-block;
width: auto;
float: right;
cursor: pointer;
}
.menulinkinline {
display: inline-block;
width: auto;
cursor: pointer;
}
.menulinkinactive {
display: inline-block;
width: auto;
cursor: default;
}
.menulink, .menulink:link, .menulink:visited, .menulink:active {
color: #0000EE;
text-decoration: none;
}
.menulink:hover {
color: #551A8B;
text-decoration: none;
}
.menulinkinactive, .menulinkinactive:link, .menulinkinactive:visited, .menulinkinactive:active, .menulinkinactive:hover {
color: #000000;
text-decoration: none;
}
.status {
grid-area: status;
overflow-x: auto;
overflow-y: scroll;
border-bottom: 1px solid #cecece;
}
.footer {
grid-area: footer;
}
.wrapper, .mobilewrapper {
margin: 0;
width: 100%;
height: 100%;
font-size: 1.2em;
}
.wrapper {
display: grid;
grid-gap: 0;
grid-template-columns: 1fr;
grid-template-rows: min-content 1fr min-content min-content;
grid-template-areas: "header" "buffer" "status" "footer";
}
.mobilewrapper {
display: none;
grid-gap: 0;
grid-template-columns: 1fr;
grid-template-rows: min-content 1fr;
grid-template-areas: "header" "mobileside";
}
.sideleft {
display: none;
grid-area: sideleft;
overflow-x: auto;
@ -30,6 +123,7 @@ nav ul, aside ul {
}
.sideright {
display: none;
grid-area: sideright;
overflow-x: auto;
@ -38,123 +132,100 @@ nav ul, aside ul {
border-bottom: 1px solid #cecece;
}
.status {
grid-area: status;
overflow-x: auto;
overflow-y: scroll;
.mobilesideleft {
border-bottom: 1px solid #cecece;
}
.footer {
grid-area: footer;
}
.wrapper {
margin: 0;
width: 100vw;
min-height: 100vh;
display: grid;
grid-gap: 0;
grid-template-columns: 1fr;
grid-template-rows: min-content min-content min-content 1fr min-content min-content;
grid-template-areas: "sideleft" "sideright" "header" "content" "status" "footer";
font-size: 1.2em;
}
@media (min-width: 500px) {
.wrapper {
height: 100vh;
grid-template-columns: 1fr 4fr;
grid-template-rows: min-content 1fr 1fr min-content min-content;
grid-template-areas: "sideleft header" "sideleft content" "sideright content" "status content" "footer footer";
}
#voiceptt {
height: 100px !important;
}
.sideleft, .sideright, .status {
min-width: 200px;
}
}
@media (min-width: 750px) {
.wrapper {
height: 100vh;
grid-template-columns: 1fr 5fr 1fr;
grid-template-rows: min-content 1fr min-content min-content;
grid-template-areas: "sideleft header sideright" "sideleft content sideright" "status content sideright" "footer footer footer"
}
#voiceptt {
height: 100px !important;
}
.sideleft, .sideright, .status {
min-width: 200px;
}
}
.sideleft, .sideright, .status, .content {
.sideleft, .sideright, .mobilesideleft, .mobilesideright, #voicestatus, #userstatus, .buffer {
padding: 7px;
}
#voicestatus {
border-bottom: 1px solid #cecece;
}
.menucontainer, .mobilemenucontainer {
display: inline-block;
width: 37px;
padding: 2px 7px 2px 2px;
cursor: pointer;
vertical-align: top;
}
.hamburger {
width: 100%;
height: 5px;
background-color: black;
margin-top: 5px;
cursor: pointer;
}
.hamburgerfirst {
margin-top: 0px;
}
.header {
padding: 10px;
overflow-x: auto;
overflow-y: scroll;
border-bottom: 1px solid #cecece;
}
.content {
.buffer {
padding-left: 10px;
padding-right: 10px;
border-bottom: 1px solid #cecece;
}
button {
border: 1px solid black;
padding: 7px;
}
#mainheader {
.mainheader {
display: inline-block;
font-size: 1.5em;
}
button, #subheader, #chathistory, #chatinput, .status {
.subheader {
font-size: 1em;
color: #444444;
}
button, .buffer, #chatinput, .status {
font-size: 1.25em;
}
#header, #status {
vertical-align: center;
.sideheading {
font-weight: bold;
}
.header {
border-right: 1px solid #cecece;
.header, .header > *, .status, .status > * {
vertical-align: baseline;
}
.headericon {
display: inline-block;
width: 25px;
font-size: 1.15em;
}
.widelinks a {
display: block;
}
#chathistory {
.sideheading {
margin-bottom: 10px;
}
.buffer {
overflow-x: auto;
overflow-y: scroll;
min-height: 150px;
border: 1px solid #cecece;
}
#chatinput {
display:block;
display: block;
width: 100%;
height: 100%;
border: 0;
resize: none;
@ -180,3 +251,61 @@ button, #subheader, #chathistory, #chatinput, .status {
width: 27px;
padding-left: 4px;
}
@media (min-width: 500px) {
.wrapper {
height: 100vh;
grid-template-columns: 1fr 4fr;
grid-template-rows: min-content 1fr 1fr min-content min-content;
grid-template-areas: "sideleft header" "sideleft buffer" "sideright buffer" "status buffer" "footer footer";
}
#voiceptt {
height: 100px;
}
.sideleft, .sideright, .status {
min-width: 200px;
}
.sideleft {
display: grid;
}
.sideright {
display: grid;
}
.menucontainer {
display: none;
}
}
@media (min-width: 750px) {
.wrapper {
height: 100vh;
grid-template-columns: 1fr 5fr 1fr;
grid-template-rows: min-content 1fr min-content min-content;
grid-template-areas: "sideleft header sideright" "sideleft buffer sideright" "status buffer sideright" "footer footer footer"
}
#voiceptt {
height: 100px;
}
.sideleft, .sideright, .status {
min-width: 200px;
}
.sideleft {
display: grid;
}
.sideright {
display: grid;
}
.menucontainer {
display: none;
}
}

View File

@ -5,7 +5,6 @@ var ReconnectDelay = 0;
var reconnectTimeout;
var connected;
var chatprefix = "";
var voice = false;
var ptt = false;
var nickname = "Anonymous";
@ -33,13 +32,16 @@ var RTCICEServers = [{urls: 'stun:stun.l.google.com:19302'}];
var audioTrack;
var shownPTTHelp = false;
var muteOnMouseUp = true;
var menuOpen = false;
var lastPing = 0;
var userPing = 0;
var userPing = -1;
var allChannels;
var voiceChannel = 0;
var allChannelsSorted;
var channelBuffers = [];
var textChannel = -1;
var voiceChannel = -1;
var disableVoice = false;
var voiceCompatibilityNotice = "Sorry, your browser does not support WebRTC. This is required join voice channels. Firefox (recommended) and Chrome support WebRTC.";
@ -69,9 +71,8 @@ var MessageChannels = 131;
var MessageUsers = 132;
var ChannelUnknown = 0;
var ChannelAll = 1;
var ChannelText = 2;
var ChannelVoice = 3;
var ChannelText = 1;
var ChannelVoice = 2;
var tagsToReplace = {
'&': '&amp;',
@ -79,6 +80,8 @@ var tagsToReplace = {
'>': '&gt;'
};
var clickEventType = ((document.ontouchstart !== null) ? 'mousedown' : 'touchstart');
$(document).keydown(HandleInput);
$(document).keyup(HandleInput);
@ -91,6 +94,9 @@ function HandleInput(e) {
}
e.preventDefault();
} else if (e.which >= 32 && e.which <= 126) {
$("#chatinput").focus();
// Do not prevent default
} else if ((e.which == 13 || e.which == 176) && e.type == "keydown" && !e.shiftKey) {
if (!$("#chatinput").is(":focus")) {
return;
@ -100,7 +106,7 @@ function HandleInput(e) {
if ($("#chatinput").val() == "/debugstats") {
enableStats();
} else {
w(MessageChat, $("#chatinput").val());
writeSocket({T: MessageChat, C: textChannel, M: btoa($("#chatinput").val())});
}
}
@ -118,31 +124,19 @@ $(document).ready(function () {
disableVoice = true;
}
$("#voiceptt").on("touchstart", function (e) {
muteOnMouseUp = false;
$("#voiceptt").on(clickEventType, function (e) {
StartPTT();
$("#chatinput").focus();
return false;
});
$("#voiceptt").on("mousedown", function (e) {
muteOnMouseUp = true;
StartPTT();
$("#chatinput").focus();
$("#voiceptt").on("mouseup touchend", function (e) {
StopPTT();
return false;
});
$(document).on("mouseup", function (e) {
if (!muteOnMouseUp) {
return;
}
StopPTT();
return false;
@ -154,18 +148,33 @@ $(document).ready(function () {
return false;
});
$("#chatinputcontainer,#voicepttcontainer").on("click touchstart", function (e) {
$("#chatinputcontainer,#voicepttcontainer").on(clickEventType, function (e) {
$("#chatinput").focus();
return false;
});
$("#chatinput").on("touchstart", function (e) {
$("#chatinput").on(clickEventType, function (e) {
$("#chatinput").focus();
return false;
});
$(".menucontainer,.mobilemenucontainer").on(clickEventType, function (e) {
menuOpen = !menuOpen;
if (menuOpen) {
$(".wrapper").css('display', 'none');
$(".mobilewrapper").css('display', 'grid');
} else {
$(".mobilewrapper").css('display', 'none');
$(".wrapper").css('display', 'grid');
}
updateHeader();
return false;
});
window.setInterval(() => {
if (!webSocketReady()) {
return;
@ -195,7 +204,7 @@ function createPeerConnection(id) {
pc.ontrack = function (event) {
console.log(`PC ${id} onTrack ${event.streams.length} ${event.streams[0].id} ${event.streams[0].getAudioTracks().length}`);
if (id > 0) {
if (id == 0) {
pc.addTransceiver(event.streams[0].getAudioTracks()[0], {'direction': 'sendrecv'});
}
@ -219,7 +228,7 @@ function createPeerConnection(id) {
if (id == 0) {
navigator.mediaDevices.getUserMedia(RTCConstraints).then(localStream => {
audioTracks = localStream.getAudioTracks();
var audioTracks = localStream.getAudioTracks();
if (audioTracks.length > 0) {
console.log(`PC ${id} Using Audio device: ${audioTracks[0].label} - tracks available: ${audioTracks}`);
}
@ -231,7 +240,7 @@ function createPeerConnection(id) {
pc.createOffer(RTCOfferOptions)
.then(onRTCDescription(id), onRTCDescriptionError(id));
}).catch(Log);
}).catch(FatalAudioError);
} else {
pc.createOffer(RTCOfferOptions)
.then(onRTCDescription(id), onRTCDescriptionError(id));
@ -250,6 +259,10 @@ function onRTCDescription(id) {
return function (desc) {
console.log(`PC ${id} Offer received \n${desc.sdp}`);
if (peerConnections.length < 3) {
peerConnections.push(createPeerConnection(peerConnections.length));
}
if (peerConnections.length < id) {
return;
}
@ -272,7 +285,7 @@ function Connect() {
return;
}
userPing = 0;
userPing = -1;
userListStatus = 'Connecting...';
updateStatus();
@ -295,14 +308,17 @@ function Connect() {
socket.onerror = function (e) {
Log("Connection error");
console.log(e);
QuitVoice();
};
socket.onopen = function (e) {
if (reconnectTimeout != null) {
clearTimeout(reconnectTimeout);
}
// TODO Update after receiving channels
updateHeader();
userListStatus = "";
updateStatus();
w(MessageNick, nickname);
@ -320,6 +336,8 @@ function Connect() {
p.M = atob(p.M);
}
console.log("Read ", p);
if (p.T == MessagePong) {
if (parseInt(p.M, 10) == lastPing) {
userPing = Date.now() - lastPing;
@ -333,10 +351,6 @@ function Connect() {
var pc = peerConnections[p.PC];
pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: p.M}));
if (peerConnections.length < 3) {
peerConnections.push(createPeerConnection(peerConnections.length));
}
} else if (p.T == MessageConnect) {
if (p.N === undefined) {
return;
@ -372,9 +386,14 @@ function Connect() {
return;
}
Log("&lt;" + escapeEntities(p.N) + "&gt; " + p.M);
LogChannel(p.C, "&lt;" + escapeEntities(p.N) + "&gt; " + p.M);
} else if (p.T == MessageChannels) {
allChannels = JSON.parse(p.M);
allChannelsSorted = JSON.parse(p.M);
allChannels = [];
for (let i = 0; i < allChannelsSorted.length; i++) {
allChannels[allChannelsSorted[i].ID] = allChannelsSorted[i];
}
updateChannelList();
} else if (p.T == MessageUsers) {
@ -390,35 +409,32 @@ function Connect() {
userList = userListNew;
var userListVoiceNew = '';
var userListVoiceNew;
var u = JSON.parse(p.M);
for (let i in allChannels) {
$("#userscontainer" + i).remove();
}
for (let ci in allChannels) {
$(".channelvoiceusers" + ci).html('');
for (let i = 0; i < u.length; i++) {
if (u[i].C == 0) {
continue;
userListVoiceNew = '<ul>';
;
for (let i = 0; i < u.length; i++) {
if (u[i].C != ci) {
continue;
}
userListVoiceNew += '<li>';
if (voice && u[i].C == voiceChannel) {
userListVoiceNew += '<span id="voiceindicator' + u[i].ID + '"><div class="voiceinactive">&#128264;</div></span> ';
}
userListVoiceNew += u[i].N + '</li>';
}
if ($("#userscontainer" + u[i].C).length == 0) {
$('<li/>')
.attr('id', 'userscontainer' + u[i].C)
.insertAfter($("#channelvoice" + u[i].C));
if (userListVoiceNew != '<ul>') {
userListVoiceNew += '<li style="margin: 0 !important;height: 7px;">&nbsp;</li></ul>';
$('<ul/>')
.attr('id', 'users' + u[i].C)
.appendTo($("#userscontainer" + u[i].C));
$(".channelvoiceusers" + ci).each(function (index) {
$(this).html(userListVoiceNew);
});
}
userListVoiceNew = '<li>';
if (voice && u[i].C == voiceChannel) {
userListVoiceNew += '<span id="voiceindicator' + u[i].ID + '"><div class="voiceinactive">&#128264;</div></span> ';
}
userListVoiceNew += u[i].N + '</li>';
$("#users" + u[i].C).append(userListVoiceNew);
}
updateUserList();
@ -439,6 +455,8 @@ function Connect() {
socket.onclose = function (e) {
connected = false;
Log("Disconnected");
QuitVoice();
if (ReconnectDelay < 0 || reconnectTimeout != null) {
return;
}
@ -477,17 +495,41 @@ function waitForSocketConnection(socket, callback) {
}
function Log(msg) {
if (msg.charAt(0) != '<' && msg.charAt(0) != '&' && msg.charAt(1) != '&' && msg.charAt(2) != 't' && msg.charAt(3) != ';') {
LogChannel(textChannel, msg);
}
function LogChannel(ch, msg) {
if (msg.charAt && msg.charAt(0) != '<' && msg.charAt(0) != '&' && msg.charAt(1) != '&' && msg.charAt(2) != 't' && msg.charAt(3) != ';') {
msg = '* ' + msg;
}
var date = new Date();
$('#chathistory').append(chatprefix + date.getHours() + ":" + (date.getMinutes() < 10 ? "0" : "") + date.getMinutes() + " " + msg + "\n")
$('#chathistory').scrollTop($('#chathistory').prop("scrollHeight"));
msg = date.getHours() + ":" + (date.getMinutes() < 10 ? "0" : "") + date.getMinutes() + " " + msg + "\n";
if (chatprefix == "") {
chatprefix = "<br>";
if (ch >= 0) {
if (channelBuffers[ch] === undefined) {
channelBuffers[ch] = msg;
} else {
channelBuffers[ch] += '<br>' + msg;
}
if (ch != textChannel) {
return;
}
}
if ($('.buffer').html().trim() != "") {
$('.buffer').append("<br>");
}
$('.buffer').append(msg);
$('.buffer').scrollTop($('.buffer').prop("scrollHeight"));
}
function FatalAudioError(msg) {
QuitVoice();
alert(msg);
Log(msg);
}
function StartPTT() {
@ -499,8 +541,11 @@ function StartPTT() {
$("#voiceptt").html('Transmitting...');
var sender = peerConnections[0].getSenders()[0];
sender.replaceTrack(audioTrack);
var senders = peerConnections[0].getSenders();
if (senders.length == 0) {
return;
}
senders[0].replaceTrack(audioTrack);
updateStatus();
}
@ -514,12 +559,45 @@ function StopPTT() {
$("#voiceptt").html('Push-To-Talk');
var sender = peerConnections[0].getSenders()[0];
sender.replaceTrack(null);
var senders = peerConnections[0].getSenders();
if (senders.length == 0) {
return;
}
senders[0].replaceTrack(null);
updateStatus();
}
function ToggleVoiceChannel(channelID) {
if (voiceChannel == channelID) {
QuitVoice();
return false;
}
if (disableVoice) {
alert(voiceCompatibilityNotice);
return false;
}
voiceChannel = channelID;
JoinVoice(voiceChannel);
return false;
}
function JoinText(channelID) {
textChannel = parseInt(channelID);
updateHeader();
updateBuffer();
for (let i in allChannels) {
$(".jointext" + i).each(function (index) {
$(this).removeClass('menulink');
$(this).removeClass('menulinkinactive');
$(this).addClass(i != textChannel ? 'menulink' : 'menulinkinactive');
});
}
}
function JoinVoice(channelID) {
if (!webSocketReady()) {
return;
@ -527,7 +605,14 @@ function JoinVoice(channelID) {
voiceChannel = parseInt(channelID);
for (let i in allChannels) {
$("#joinvoice" + i).html(i != voiceChannel ? 'Join' : 'Quit');
$(".joinvoice" + i).each(function (index) {
$(this).removeClass('menulink');
$(this).removeClass('menulinkinactive');
$(this).addClass(i != voiceChannel ? 'menulink' : 'menulinkinactive');
});
$("#voicestatus").each(function (index) {
$(this).css('display', 'block');
});
}
voice = true;
@ -536,12 +621,19 @@ function JoinVoice(channelID) {
peerConnections.push(createPeerConnection(0));
}
socket.send(JSON.stringify({T: MessageJoin, C: parseInt(channelID)}));
writeSocket({T: MessageJoin, C: parseInt(channelID)});
}
function QuitVoice() {
for (let i in allChannels) {
$("#joinvoice" + i).html('Join');
$(".joinvoice" + i).each(function (index) {
$(this).removeClass('menulink');
$(this).removeClass('menulinkinactive');
$(this).addClass('menulink');
});
$("#voicestatus").each(function (index) {
$(this).css('display', 'none');
});
}
voice = false;
@ -559,9 +651,31 @@ function QuitVoice() {
updateStatus();
}
function updateHeader() {
if (menuOpen) {
$(".mainheader").html('Menu');
$(".subheader").html('');
} else if (allChannels !== undefined) {
$(".mainheader").html(allChannels[textChannel].Name);
$(".subheader").html(allChannels[textChannel].Topic);
} else {
$(".mainheader").html('Loading...');
$(".subheader").html('');
}
}
function updateBuffer() {
if (textChannel == -1 || channelBuffers[textChannel] === undefined) {
$('.buffer').html('');
return;
}
$('.buffer').html(channelBuffers[textChannel]);
}
function updateStatus() {
var out = '';
if (userPing > 0) {
if (userPing >= 0) {
out += userPing + 'ms ping';
}
@ -574,85 +688,74 @@ function updateStatus() {
}
$('#userstatus').html(out);
$('#togglevoice').html(peerConnections.length > 0 ? 'Quit' : 'Join');
}
function updateChannelList() {
var c;
var channelListNew = '<ul>';
var channelListNew = '<ul class="sidemenu">';
channelListNew += '<li style="margin-bottom: 10px;"><div class="headericon">&#128240;</div> Text Channels</li>';
for (let i in allChannels) {
c = allChannels[i];
for (let i in allChannelsSorted) {
c = allChannelsSorted[i];
if (c.Type != ChannelAll && c.Type != ChannelText) {
if (c.Type != ChannelText) {
continue;
}
channelListNew += '<li id="channeltext' + c.ID + '">' + c.Name + '</li>';
if (textChannel == -1) {
JoinText(parseInt(c.ID));
}
channelListNew += '<li class="sideheading channeltext' + c.ID + '">';
channelListNew += '<a href="#" onclick="JoinText(' + parseInt(c.ID) + ');return false;" ontouchstart="JoinText(' + parseInt(c.ID) + ');return false;" class="menulink jointext' + c.ID + '">&#128240; ';
channelListNew += c.Name;
channelListNew += '</a>';
channelListNew += '</li>';
}
channelListNew += '<li style="margin-bottom: 10px;">&nbsp;</li><li style="margin-bottom: 10px;"><div class="headericon">&#128266;</div> Voice Channels</li>';
for (let i in allChannels) {
c = allChannels[i];
channelListNew += '<li>&nbsp;</li>';
for (let i in allChannelsSorted) {
c = allChannelsSorted[i];
if (c.Type != ChannelAll && c.Type != ChannelVoice) {
if (c.Type != ChannelVoice) {
continue;
}
channelListNew += '<li id="channelvoice' + c.ID + '">' + c.Name + ' <a href="#" id="joinvoice' + c.ID + '">' + (parseInt(c.ID) != voiceChannel ? 'Join' : 'Quit') + '</a></li>';
channelListNew += '<li class="sideheading channelvoice' + c.ID + '">';
channelListNew += '<a href="#" onclick="JoinVoice(' + parseInt(c.ID) + ');return false;" ontouchstart="JoinVoice(' + parseInt(c.ID) + ');return false;" class="menulink joinvoice' + c.ID + '">&#128266; ';
channelListNew += c.Name;
channelListNew += '</a>';
channelListNew += '</li>';
channelListNew += '<li class="channelvoiceusers' + c.ID + '"></li>';
}
channelListNew += '</ul>';
channelList = channelListNew;
$("#sideleft").html(channelList);
for (let i in allChannels) {
c = allChannels[i];
if (c.Type != ChannelAll && c.Type != ChannelVoice) {
continue;
}
$("#joinvoice" + c.ID).on("click touchstart", function (e) {
if (voiceChannel == parseInt($(this).attr('id').substring(9))) {
QuitVoice();
return false;
}
if (disableVoice) {
alert(voiceCompatibilityNotice);
return false;
}
voiceChannel = parseInt($(this).attr('id').substring(9));
JoinVoice(voiceChannel);
return false;
});
}
$(".sideleft,.mobilesideleft").html(channelList);
JoinText(textChannel);
}
function updateUserList() {
$('#sideright').html(userList);
$('.sideright,.mobilesideright').html(userList);
}
function w(t, m) {
if (!webSocketReady()) {
return;
}
socket.send(JSON.stringify({T: t, M: btoa(m)}));
writeSocket({T: t, M: btoa(m)});
}
function wpc(pc, t, m) {
writeSocket({PC: parseInt(pc), T: t, M: btoa(m)});
}
function writeSocket(p) {
console.log("Write ", p);
if (!webSocketReady()) {
return;
}
socket.send(JSON.stringify({PC: parseInt(pc), T: t, M: btoa(m)}));
socket.send(JSON.stringify(p))
}
function escapeEntitiesCallback(tag) {

View File

@ -9,17 +9,23 @@
</head>
<body>
<div class="wrapper">
<nav class="sideleft" id="sideleft">
<nav class="sideleft">
</nav>
<article class="content" id="chathistory">
<article class="buffer">
</article>
<aside class="sideright" id="sideright">
<aside class="sideright">
</aside>
<div class="header" id="header">
<div style="display: inline-block;float: left;" id="mainheader">harmony</div>
<div style="display: inline-block;float: right;" id="subheader">Loading...</div>
<div class="header">
<div class="menucontainer">
<div class="hamburger hamburgerfirst"></div>
<div class="hamburger"></div>
<div class="hamburger"></div>
</div>
<span class="mainheader">Loading...</span>
<span class="subheader"></span>
</div>
<div class="status">
<div id="voicestatus" style="display: none;"><a href="#" class="menulink menulinkinline quitvoice" onclick="QuitVoice();return false;">Quit voice chat</a></div>
<div id="userstatus">Loading...</div>
</div>
<footer class="footer">
@ -31,6 +37,22 @@
</div>
</footer>
</div>
<div class="mobilewrapper">
<div class="header">
<div class="mobilemenucontainer">
<div class="hamburger hamburgerfirst"></div>
<div class="hamburger"></div>
<div class="hamburger"></div>
</div>
<span class="mainheader">Loading...</span>
<span class="subheader"></span>
</div>
<nav class="mobileside">
<aside class="mobilesideleft"></aside>
<aside class="mobilesideright"></aside>
</nav>
</nav>
</div>
<div style="display: none" id="hidden">
</div>
</body>

View File

@ -98,14 +98,17 @@ func (w *WebInterface) createChannels() {
w.ChannelsLock.Lock()
defer w.ChannelsLock.Unlock()
ch := agent.NewChannel(w.nextChannelID(), agent.ChannelAll)
ch.Name = "lobby"
ch.Topic = "harmony demo server"
w.Channels[ch.ID] = ch
// TODO Load channels from database
}
func (w *WebInterface) AddChannel(t agent.ChannelType, name string, topic string) {
w.ChannelsLock.Lock()
defer w.ChannelsLock.Unlock()
ch := agent.NewChannel(w.nextChannelID(), t)
ch.Name = name
ch.Topic = topic
ch = agent.NewChannel(w.nextChannelID(), agent.ChannelAll)
ch.Name = "alt"
ch.Topic = "alt demo channel"
w.Channels[ch.ID] = ch
}
@ -203,7 +206,7 @@ func (w *WebInterface) handleRead(c *agent.Client) {
w.ClientsLock.Lock()
for _, wc := range w.Clients {
wc.Out <- &agent.Message{S: c.ID, N: c.Name, T: agent.MessageChat, M: msg.M}
wc.Out <- &agent.Message{S: c.ID, C: msg.C, N: c.Name, T: agent.MessageChat, M: msg.M}
}
w.ClientsLock.Unlock()
@ -384,11 +387,13 @@ func (w *WebInterface) updateUserList() {
}
func (w *WebInterface) sendChannelList(c *agent.Client) {
var channelList = make(agent.ChannelList)
var channelList agent.ChannelList
for _, ch := range w.Channels {
channelList[ch.ID] = &agent.ChannelListing{ID: ch.ID, Type: ch.Type, Name: ch.Name, Topic: ch.Topic}
channelList = append(channelList, &agent.ChannelListing{ID: ch.ID, Type: ch.Type, Name: ch.Name, Topic: ch.Topic})
}
sort.Sort(channelList)
msg := agent.Message{T: agent.MessageChannels}
var err error