mirror of
https://github.com/thelounge/thelounge.git
synced 2024-05-30 12:22:30 +02:00
Add MONITOR support and status icons for queries
This commit is contained in:
parent
8606d717aa
commit
5283d10cfb
|
@ -2,6 +2,11 @@
|
||||||
<!-- TODO: investigate -->
|
<!-- TODO: investigate -->
|
||||||
<ChannelWrapper ref="wrapper" v-bind="$props">
|
<ChannelWrapper ref="wrapper" v-bind="$props">
|
||||||
<span class="name">{{ channel.name }}</span>
|
<span class="name">{{ channel.name }}</span>
|
||||||
|
<StatusIcon
|
||||||
|
v-if="channel.type === 'query' && network.status.connected"
|
||||||
|
:online="channel.isOnline"
|
||||||
|
:away="!!channel.userAway"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="channel.unread"
|
v-if="channel.unread"
|
||||||
:class="{highlight: channel.highlight && !channel.muted}"
|
:class="{highlight: channel.highlight && !channel.muted}"
|
||||||
|
@ -34,11 +39,13 @@ import roundBadgeNumber from "../js/helpers/roundBadgeNumber";
|
||||||
import useCloseChannel from "../js/hooks/use-close-channel";
|
import useCloseChannel from "../js/hooks/use-close-channel";
|
||||||
import {ClientChan, ClientNetwork} from "../js/types";
|
import {ClientChan, ClientNetwork} from "../js/types";
|
||||||
import ChannelWrapper from "./ChannelWrapper.vue";
|
import ChannelWrapper from "./ChannelWrapper.vue";
|
||||||
|
import StatusIcon from "./StatusIcon.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Channel",
|
name: "Channel",
|
||||||
components: {
|
components: {
|
||||||
ChannelWrapper,
|
ChannelWrapper,
|
||||||
|
StatusIcon,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
network: {
|
network: {
|
||||||
|
|
|
@ -21,6 +21,12 @@
|
||||||
<span class="title" :aria-label="'Currently open ' + channel.type">{{
|
<span class="title" :aria-label="'Currently open ' + channel.type">{{
|
||||||
channel.name
|
channel.name
|
||||||
}}</span>
|
}}</span>
|
||||||
|
<StatusIcon
|
||||||
|
v-if="channel.type === 'query'"
|
||||||
|
:online="channel.isOnline"
|
||||||
|
:away="!!channel.userAway"
|
||||||
|
tooltip-dir="e"
|
||||||
|
/>
|
||||||
<div v-if="channel.editTopic === true" class="topic-container">
|
<div v-if="channel.editTopic === true" class="topic-container">
|
||||||
<input
|
<input
|
||||||
ref="topicInput"
|
ref="topicInput"
|
||||||
|
@ -136,6 +142,7 @@ import ListIgnored from "./Special/ListIgnored.vue";
|
||||||
import {defineComponent, PropType, ref, computed, watch, nextTick, onMounted, Component} from "vue";
|
import {defineComponent, PropType, ref, computed, watch, nextTick, onMounted, Component} from "vue";
|
||||||
import type {ClientNetwork, ClientChan} from "../js/types";
|
import type {ClientNetwork, ClientChan} from "../js/types";
|
||||||
import {useStore} from "../js/store";
|
import {useStore} from "../js/store";
|
||||||
|
import StatusIcon from "./StatusIcon.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Chat",
|
name: "Chat",
|
||||||
|
@ -146,6 +153,7 @@ export default defineComponent({
|
||||||
ChatUserList,
|
ChatUserList,
|
||||||
SidebarToggle,
|
SidebarToggle,
|
||||||
MessageSearchForm,
|
MessageSearchForm,
|
||||||
|
StatusIcon,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
network: {type: Object as PropType<ClientNetwork>, required: true},
|
network: {type: Object as PropType<ClientNetwork>, required: true},
|
||||||
|
|
|
@ -31,16 +31,15 @@
|
||||||
:class="['user-mode', getModeClass(String(mode))]"
|
:class="['user-mode', getModeClass(String(mode))]"
|
||||||
>
|
>
|
||||||
<template v-if="userSearchInput.length > 0">
|
<template v-if="userSearchInput.length > 0">
|
||||||
<!-- eslint-disable -->
|
|
||||||
<Username
|
<Username
|
||||||
v-for="user in users"
|
v-for="user in users"
|
||||||
:key="user.original.nick + '-search'"
|
:key="user.original.nick + '-search'"
|
||||||
:on-hover="hoverUser"
|
:on-hover="hoverUser"
|
||||||
:active="user.original === activeUser"
|
:active="user.original === activeUser"
|
||||||
:user="(user.original as any)"
|
:user="(user.original as any)"
|
||||||
v-html="user.string"
|
:html="user.string"
|
||||||
|
:include-status-icon="true"
|
||||||
/>
|
/>
|
||||||
<!-- eslint-enable -->
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Username
|
<Username
|
||||||
|
@ -49,6 +48,7 @@
|
||||||
:on-hover="hoverUser"
|
:on-hover="hoverUser"
|
||||||
:active="user === activeUser"
|
:active="user === activeUser"
|
||||||
:user="user"
|
:user="user"
|
||||||
|
:include-status-icon="true"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,6 +56,123 @@
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.userlist {
|
||||||
|
border-left: 1px solid #e7e7e7;
|
||||||
|
width: 180px;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-shrink: 0;
|
||||||
|
touch-action: pan-y;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userlist .count {
|
||||||
|
background: #fafafa;
|
||||||
|
height: 45px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.userlist .search {
|
||||||
|
color: var(--body-color);
|
||||||
|
appearance: none;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
font: inherit;
|
||||||
|
outline: 0;
|
||||||
|
padding: 13px;
|
||||||
|
padding-right: 30px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userlist .names {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
|
touch-action: pan-y;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewport.userlist-open #chat .userlist {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .names .user {
|
||||||
|
display: block;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 0 16px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .user-mode {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .user-mode::before {
|
||||||
|
background: var(--window-bg-color);
|
||||||
|
color: var(--body-color-muted);
|
||||||
|
display: block;
|
||||||
|
font-size: 0.85em;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 5px 16px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .user-mode.owner::before {
|
||||||
|
content: "Owners";
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .user-mode.admin::before {
|
||||||
|
content: "Administrators";
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .user-mode.op::before {
|
||||||
|
content: "Operators";
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .user-mode.half-op::before {
|
||||||
|
content: "Half-Operators";
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .user-mode.voice::before {
|
||||||
|
content: "Voiced";
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .user-mode.normal::before {
|
||||||
|
content: "Users";
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .user-mode-search::before {
|
||||||
|
content: "Search Results";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status icon */
|
||||||
|
#chat .names .status {
|
||||||
|
margin-left: -3px;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#chat .userlist {
|
||||||
|
background-color: var(--window-bg-color);
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
transform: translateX(180px);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewport.userlist-open #chat .userlist {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {filter as fuzzyFilter} from "fuzzy";
|
import {filter as fuzzyFilter} from "fuzzy";
|
||||||
import {computed, defineComponent, nextTick, PropType, ref} from "vue";
|
import {computed, defineComponent, nextTick, PropType, ref} from "vue";
|
||||||
|
|
|
@ -4,13 +4,15 @@
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Username :user="message.from" />
|
<Username :user="message.from" />
|
||||||
is away
|
is away
|
||||||
<i class="away-message">(<ParsedMessage :network="network" :message="message" />)</i>
|
<i v-if="awayMessage" class="away-message"
|
||||||
|
>(<ParsedMessage :network="network" :message="message" />)</i
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {defineComponent, PropType} from "vue";
|
import {computed, defineComponent, PropType} from "vue";
|
||||||
import type {ClientNetwork, ClientMessage} from "../../js/types";
|
import type {ClientNetwork, ClientMessage} from "../../js/types";
|
||||||
import ParsedMessage from "../ParsedMessage.vue";
|
import ParsedMessage from "../ParsedMessage.vue";
|
||||||
import Username from "../Username.vue";
|
import Username from "../Username.vue";
|
||||||
|
@ -31,5 +33,11 @@ export default defineComponent({
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
setup(props) {
|
||||||
|
const awayMessage = computed(() => props.message.text.trim());
|
||||||
|
return {
|
||||||
|
awayMessage,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -98,6 +98,14 @@
|
||||||
/>
|
/>
|
||||||
Enable autocomplete
|
Enable autocomplete
|
||||||
</label>
|
</label>
|
||||||
|
<label class="opt">
|
||||||
|
<input
|
||||||
|
:checked="store.state.settings.statusIcons"
|
||||||
|
type="checkbox"
|
||||||
|
name="statusIcons"
|
||||||
|
/>
|
||||||
|
Enable status icons
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="opt">
|
<label class="opt">
|
||||||
|
|
81
client/components/StatusIcon.vue
Normal file
81
client/components/StatusIcon.vue
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="store.state.settings.statusIcons"
|
||||||
|
:class="['status', 'tooltipped tooltipped-no-touch', tooltipDirClass]"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
>
|
||||||
|
<span :class="{online: online, offline: !online, away: away}" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.status {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online::after,
|
||||||
|
.away::after,
|
||||||
|
.offline::after {
|
||||||
|
content: "";
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online::after {
|
||||||
|
background-color: #2ecc40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline::after {
|
||||||
|
background-color: #ff4136;
|
||||||
|
}
|
||||||
|
|
||||||
|
.away::after {
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {computed, PropType, defineComponent} from "vue";
|
||||||
|
import {useStore} from "../js/store";
|
||||||
|
export default defineComponent({
|
||||||
|
name: "StatusIcon",
|
||||||
|
props: {
|
||||||
|
online: Boolean,
|
||||||
|
away: Boolean,
|
||||||
|
tooltipDir: String as PropType<"n" | "s" | "e" | "w">,
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const tooltipDirClass = computed(
|
||||||
|
() => `tooltipped-${props.tooltipDir ? props.tooltipDir : "w"}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const ariaLabel = computed(() => {
|
||||||
|
if (props.away) {
|
||||||
|
return "Away";
|
||||||
|
} else if (props.online) {
|
||||||
|
return "Online";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Offline";
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tooltipDirClass,
|
||||||
|
ariaLabel,
|
||||||
|
store,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -6,8 +6,25 @@
|
||||||
v-on="onHover ? {mouseenter: hover} : {}"
|
v-on="onHover ? {mouseenter: hover} : {}"
|
||||||
@click.prevent="openContextMenu"
|
@click.prevent="openContextMenu"
|
||||||
@contextmenu.prevent="openContextMenu"
|
@contextmenu.prevent="openContextMenu"
|
||||||
><slot>{{ mode }}{{ user.nick }}</slot></span
|
><slot v-if="!html">
|
||||||
>
|
<StatusIcon
|
||||||
|
v-if="includeStatusIcon"
|
||||||
|
:away="!!user.away"
|
||||||
|
:tooltip-dir="'e'"
|
||||||
|
:online="true"
|
||||||
|
/>
|
||||||
|
{{ mode }}{{ user.nick }}
|
||||||
|
</slot>
|
||||||
|
<slot v-else>
|
||||||
|
<StatusIcon
|
||||||
|
v-if="includeStatusIcon"
|
||||||
|
:away="!!user.away"
|
||||||
|
:tooltip-dir="'e'"
|
||||||
|
:online="true"
|
||||||
|
/>
|
||||||
|
<span class="nick" v-html="html"></span>
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -15,7 +32,8 @@ import {computed, defineComponent, PropType} from "vue";
|
||||||
import {UserInMessage} from "../../src/models/msg";
|
import {UserInMessage} from "../../src/models/msg";
|
||||||
import eventbus from "../js/eventbus";
|
import eventbus from "../js/eventbus";
|
||||||
import colorClass from "../js/helpers/colorClass";
|
import colorClass from "../js/helpers/colorClass";
|
||||||
import type {ClientChan, ClientNetwork, ClientUser} from "../js/types";
|
import type {ClientChan, ClientNetwork} from "../js/types";
|
||||||
|
import StatusIcon from "./StatusIcon.vue";
|
||||||
|
|
||||||
type UsernameUser = Partial<UserInMessage> & {
|
type UsernameUser = Partial<UserInMessage> & {
|
||||||
mode?: string;
|
mode?: string;
|
||||||
|
@ -24,6 +42,9 @@ type UsernameUser = Partial<UserInMessage> & {
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Username",
|
name: "Username",
|
||||||
|
components: {
|
||||||
|
StatusIcon,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
user: {
|
user: {
|
||||||
// TODO: UserInMessage shouldn't be necessary here.
|
// TODO: UserInMessage shouldn't be necessary here.
|
||||||
|
@ -37,6 +58,8 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
channel: {type: Object as PropType<ClientChan>, required: false},
|
channel: {type: Object as PropType<ClientChan>, required: false},
|
||||||
network: {type: Object as PropType<ClientNetwork>, required: false},
|
network: {type: Object as PropType<ClientNetwork>, required: false},
|
||||||
|
includeStatusIcon: Boolean,
|
||||||
|
html: String,
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const mode = computed(() => {
|
const mode = computed(() => {
|
||||||
|
|
|
@ -664,10 +664,6 @@ p {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#viewport.userlist-open #chat .userlist {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
display: none;
|
display: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -1221,15 +1217,6 @@ textarea.input {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat .userlist {
|
|
||||||
border-left: 1px solid #e7e7e7;
|
|
||||||
width: 180px;
|
|
||||||
display: none;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-shrink: 0;
|
|
||||||
touch-action: pan-y;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggled via JavaScript
|
* Toggled via JavaScript
|
||||||
*/
|
*/
|
||||||
|
@ -1300,10 +1287,6 @@ textarea.input {
|
||||||
content: "\f107"; /* https://fontawesome.com/icons/angle-down?style=solid */
|
content: "\f107"; /* https://fontawesome.com/icons/angle-down?style=solid */
|
||||||
}
|
}
|
||||||
|
|
||||||
.userlist-open .chat-view[data-type="channel"] .scroll-down {
|
|
||||||
right: 196px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat .messages {
|
#chat .messages {
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
touch-action: pan-y;
|
touch-action: pan-y;
|
||||||
|
@ -1733,87 +1716,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat .userlist .count {
|
|
||||||
background: #fafafa;
|
|
||||||
height: 45px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat .userlist .search {
|
|
||||||
color: var(--body-color);
|
|
||||||
appearance: none;
|
|
||||||
border: 0;
|
|
||||||
background: none;
|
|
||||||
font: inherit;
|
|
||||||
outline: 0;
|
|
||||||
padding: 13px;
|
|
||||||
padding-right: 30px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat .userlist .names {
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
width: 100%;
|
|
||||||
touch-action: pan-y;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat .names .user {
|
|
||||||
display: block;
|
|
||||||
line-height: 1.6;
|
|
||||||
padding: 0 16px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat .user-mode {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat .user-mode::before {
|
|
||||||
background: var(--window-bg-color);
|
|
||||||
color: var(--body-color-muted);
|
|
||||||
display: block;
|
|
||||||
font-size: 0.85em;
|
|
||||||
line-height: 1.6;
|
|
||||||
padding: 5px 16px;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat .user-mode.owner::before {
|
|
||||||
content: "Owners";
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat .user-mode.admin::before {
|
|
||||||
content: "Administrators";
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat .user-mode.op::before {
|
|
||||||
content: "Operators";
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat .user-mode.half-op::before {
|
|
||||||
content: "Half-Operators";
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat .user-mode.voice::before {
|
|
||||||
content: "Voiced";
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat .user-mode.normal::before {
|
|
||||||
content: "Users";
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat .user-mode-search::before {
|
|
||||||
content: "Search Results";
|
|
||||||
}
|
|
||||||
|
|
||||||
#loading {
|
#loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -2693,20 +2595,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat .userlist {
|
|
||||||
background-color: var(--window-bg-color);
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
transform: translateX(180px);
|
|
||||||
transition: transform 0.2s;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#viewport.userlist-open #chat .userlist {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat .header .title {
|
#chat .header .title {
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,9 @@ const defaultConfig = {
|
||||||
coloredNicks: {
|
coloredNicks: {
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
statusIcons: {
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
desktopNotifications: {
|
desktopNotifications: {
|
||||||
default: false,
|
default: false,
|
||||||
sync: "never",
|
sync: "never",
|
||||||
|
|
|
@ -14,3 +14,31 @@ socket.on("users", function (data) {
|
||||||
channel.channel.usersOutdated = true;
|
channel.channel.usersOutdated = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("users:online", ({changedChannels, networkId}) => {
|
||||||
|
for (const network of store.state.networks) {
|
||||||
|
if (network.uuid === networkId) {
|
||||||
|
for (const channel of network.channels) {
|
||||||
|
if (changedChannels.includes(channel.name)) {
|
||||||
|
channel.isOnline = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("users:offline", function ({changedChannels, networkId}) {
|
||||||
|
for (const network of store.state.networks) {
|
||||||
|
if (network.uuid === networkId) {
|
||||||
|
for (const channel of network.channels) {
|
||||||
|
if (changedChannels.includes(channel.name)) {
|
||||||
|
channel.isOnline = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -20,6 +20,7 @@ import ClientManager from "./clientManager";
|
||||||
import {MessageStorage, SearchQuery} from "./plugins/messageStorage/types";
|
import {MessageStorage, SearchQuery} from "./plugins/messageStorage/types";
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
|
"account",
|
||||||
"away",
|
"away",
|
||||||
"cap",
|
"cap",
|
||||||
"connection",
|
"connection",
|
||||||
|
@ -45,6 +46,7 @@ const events = [
|
||||||
"topic",
|
"topic",
|
||||||
"welcome",
|
"welcome",
|
||||||
"whois",
|
"whois",
|
||||||
|
"users",
|
||||||
];
|
];
|
||||||
|
|
||||||
type ClientPushSubscription = {
|
type ClientPushSubscription = {
|
||||||
|
|
|
@ -47,6 +47,7 @@ class Chan {
|
||||||
muted!: boolean;
|
muted!: boolean;
|
||||||
type!: ChanType;
|
type!: ChanType;
|
||||||
state!: ChanState;
|
state!: ChanState;
|
||||||
|
isOnline?: boolean;
|
||||||
|
|
||||||
// These are added to the channel elsewhere and should not be saved.
|
// These are added to the channel elsewhere and should not be saved.
|
||||||
userAway!: boolean;
|
userAway!: boolean;
|
||||||
|
@ -73,6 +74,10 @@ class Chan {
|
||||||
users: new Map(),
|
users: new Map(),
|
||||||
muted: false,
|
muted: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.type === ChanType.QUERY) {
|
||||||
|
this.isOnline = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
|
|
@ -35,6 +35,8 @@ export enum MessageType {
|
||||||
RAW = "raw",
|
RAW = "raw",
|
||||||
PLUGIN = "plugin",
|
PLUGIN = "plugin",
|
||||||
WALLOPS = "wallops",
|
WALLOPS = "wallops",
|
||||||
|
ONLINE = "online",
|
||||||
|
OFFLINE = "offline",
|
||||||
}
|
}
|
||||||
|
|
||||||
class Msg {
|
class Msg {
|
||||||
|
@ -59,7 +61,7 @@ class Msg {
|
||||||
command!: string;
|
command!: string;
|
||||||
invitedYou!: boolean;
|
invitedYou!: boolean;
|
||||||
gecos!: string;
|
gecos!: string;
|
||||||
account!: boolean;
|
account!: string;
|
||||||
|
|
||||||
// these are all just for error:
|
// these are all just for error:
|
||||||
error!: string;
|
error!: string;
|
||||||
|
|
|
@ -107,8 +107,12 @@ class Network {
|
||||||
CHANTYPES: string[];
|
CHANTYPES: string[];
|
||||||
PREFIX: Prefix;
|
PREFIX: Prefix;
|
||||||
NETWORK: string;
|
NETWORK: string;
|
||||||
|
MONITOR: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
monitorList!: string[];
|
||||||
|
toBeMonitored!: string[];
|
||||||
|
|
||||||
// TODO: this is only available on export
|
// TODO: this is only available on export
|
||||||
hasSTSPolicy!: boolean;
|
hasSTSPolicy!: boolean;
|
||||||
|
|
||||||
|
@ -152,6 +156,8 @@ class Network {
|
||||||
chanCache: [],
|
chanCache: [],
|
||||||
ignoreList: [],
|
ignoreList: [],
|
||||||
keepNick: null,
|
keepNick: null,
|
||||||
|
monitorList: [],
|
||||||
|
toBeMonitored: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.uuid) {
|
if (!this.uuid) {
|
||||||
|
@ -288,7 +294,8 @@ class Network {
|
||||||
|
|
||||||
this.irc.requestCap([
|
this.irc.requestCap([
|
||||||
"znc.in/self-message", // Legacy echo-message for ZNC
|
"znc.in/self-message", // Legacy echo-message for ZNC
|
||||||
"znc.in/playback", // See http://wiki.znc.in/Playback
|
"znc.in/playback", // See http://wiki.znc.in/Playback,
|
||||||
|
"draft/extended-monitor", // https://ircv3.net/specs/extensions/extended-monitor
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -521,7 +528,27 @@ class Network {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
addChannel(newChan: Chan) {
|
monitor(this: NetworkWithIrcFramework, target: string) {
|
||||||
|
this.irc.addMonitor(target);
|
||||||
|
|
||||||
|
if (this.monitorList.length < this.serverOptions.MONITOR) {
|
||||||
|
this.monitorList.push(target);
|
||||||
|
} else {
|
||||||
|
// TODO: ISON fallback?
|
||||||
|
this.toBeMonitored.push(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMonitor(this: NetworkWithIrcFramework, target: string) {
|
||||||
|
this.irc.removeMonitor(target);
|
||||||
|
this.monitorList = this.monitorList.filter((monitored) => monitored !== target);
|
||||||
|
|
||||||
|
if (this.toBeMonitored.length > 0) {
|
||||||
|
this.monitor(this.toBeMonitored.shift() as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addChannel(this: NetworkWithIrcFramework, newChan: Chan) {
|
||||||
let index = this.channels.length; // Default to putting as the last item in the array
|
let index = this.channels.length; // Default to putting as the last item in the array
|
||||||
|
|
||||||
// Don't sort special channels in amongst channels/users.
|
// Don't sort special channels in amongst channels/users.
|
||||||
|
@ -544,6 +571,13 @@ class Network {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.channels.splice(index, 0, newChan);
|
this.channels.splice(index, 0, newChan);
|
||||||
|
|
||||||
|
if (newChan.type === ChanType.QUERY) {
|
||||||
|
if (!this.monitorList.includes(newChan.name)) {
|
||||||
|
this.monitor(newChan.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ class User {
|
||||||
mode!: string;
|
mode!: string;
|
||||||
away!: string;
|
away!: string;
|
||||||
nick!: string;
|
nick!: string;
|
||||||
|
account!: string;
|
||||||
lastMessage!: number;
|
lastMessage!: number;
|
||||||
|
|
||||||
constructor(attr: Partial<User>, prefix?: Prefix) {
|
constructor(attr: Partial<User>, prefix?: Prefix) {
|
||||||
|
@ -14,6 +15,7 @@ class User {
|
||||||
modes: [],
|
modes: [],
|
||||||
away: "",
|
away: "",
|
||||||
nick: "",
|
nick: "",
|
||||||
|
account: "",
|
||||||
lastMessage: 0,
|
lastMessage: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,6 +39,8 @@ class User {
|
||||||
nick: this.nick,
|
nick: this.nick,
|
||||||
modes: this.modes,
|
modes: this.modes,
|
||||||
lastMessage: this.lastMessage,
|
lastMessage: this.lastMessage,
|
||||||
|
away: this.away,
|
||||||
|
account: this.account,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
src/plugins/irc-events/account.ts
Normal file
24
src/plugins/irc-events/account.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import {IrcEventHandler} from "../../client";
|
||||||
|
|
||||||
|
import Msg, {MessageType} from "../../models/msg";
|
||||||
|
|
||||||
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
|
const client = this;
|
||||||
|
|
||||||
|
irc.on("account", function (data) {
|
||||||
|
network.channels.forEach((chan) => {
|
||||||
|
const user = chan.findUser(data.nick);
|
||||||
|
|
||||||
|
if (typeof user === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.account = data.account ? data.account : "";
|
||||||
|
chan.setUser(user);
|
||||||
|
|
||||||
|
client.emit("users", {
|
||||||
|
chan: chan.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -63,6 +63,11 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
}
|
}
|
||||||
|
|
||||||
user.away = away;
|
user.away = away;
|
||||||
|
chan.setUser(user);
|
||||||
|
|
||||||
|
client.emit("users", {
|
||||||
|
chan: chan.id,
|
||||||
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,10 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
|
|
||||||
network.channels.forEach((chan) => {
|
network.channels.forEach((chan) => {
|
||||||
if (chan.type !== ChanType.CHANNEL) {
|
if (chan.type !== ChanType.CHANNEL) {
|
||||||
|
if (chan.type === ChanType.QUERY) {
|
||||||
|
network.monitor(chan.name);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,6 +208,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
}
|
}
|
||||||
|
|
||||||
network.serverOptions.NETWORK = data.options.NETWORK;
|
network.serverOptions.NETWORK = data.options.NETWORK;
|
||||||
|
network.serverOptions.MONITOR = data.options.MONITOR;
|
||||||
|
|
||||||
client.emit("network:options", {
|
client.emit("network:options", {
|
||||||
network: network.uuid,
|
network: network.uuid,
|
||||||
|
|
|
@ -1,13 +1,32 @@
|
||||||
import Msg, {MessageType} from "../../models/msg";
|
import Msg, {MessageType} from "../../models/msg";
|
||||||
|
import Chan, {ChanState, ChanType} from "../../models/chan";
|
||||||
import User from "../../models/user";
|
import User from "../../models/user";
|
||||||
import type {IrcEventHandler} from "../../client";
|
import type {IrcEventHandler} from "../../client";
|
||||||
import {ChanState} from "../../models/chan";
|
|
||||||
|
|
||||||
export default <IrcEventHandler>function (irc, network) {
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
const client = this;
|
const client = this;
|
||||||
|
|
||||||
irc.on("join", function (data) {
|
irc.on("join", function (data) {
|
||||||
|
const performWhoOnChannel = (chan: Chan) => {
|
||||||
|
if (chan.type !== ChanType.CHANNEL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
irc.who(chan.name, (whoData) => {
|
||||||
|
for (const user of whoData.users) {
|
||||||
|
chan.setUser(
|
||||||
|
new User({
|
||||||
|
nick: user.nick,
|
||||||
|
away: user.away,
|
||||||
|
account: user.account,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
let chan = network.getChannel(data.channel);
|
let chan = network.getChannel(data.channel);
|
||||||
|
const isSelf = data.nick === irc.user.nick;
|
||||||
|
|
||||||
if (typeof chan === "undefined") {
|
if (typeof chan === "undefined") {
|
||||||
chan = client.createChannel({
|
chan = client.createChannel({
|
||||||
|
@ -26,7 +45,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
|
|
||||||
// Request channels' modes
|
// Request channels' modes
|
||||||
network.irc.raw("MODE", chan.name);
|
network.irc.raw("MODE", chan.name);
|
||||||
} else if (data.nick === irc.user.nick) {
|
} else if (isSelf) {
|
||||||
chan.state = ChanState.JOINED;
|
chan.state = ChanState.JOINED;
|
||||||
|
|
||||||
client.emit("channel:state", {
|
client.emit("channel:state", {
|
||||||
|
@ -35,7 +54,7 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = new User({nick: data.nick});
|
const user = new User({nick: data.nick, account: data.account});
|
||||||
const msg = new Msg({
|
const msg = new Msg({
|
||||||
time: data.time,
|
time: data.time,
|
||||||
from: user,
|
from: user,
|
||||||
|
@ -43,13 +62,18 @@ export default <IrcEventHandler>function (irc, network) {
|
||||||
gecos: data.gecos,
|
gecos: data.gecos,
|
||||||
account: data.account,
|
account: data.account,
|
||||||
type: MessageType.JOIN,
|
type: MessageType.JOIN,
|
||||||
self: data.nick === irc.user.nick,
|
self: isSelf,
|
||||||
});
|
});
|
||||||
chan.pushMessage(client, msg);
|
chan.pushMessage(client, msg);
|
||||||
|
|
||||||
chan.setUser(new User({nick: data.nick}));
|
chan.setUser(user);
|
||||||
client.emit("users", {
|
client.emit("users", {
|
||||||
chan: chan.id,
|
chan: chan.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If we join a channel, we perform a WHO to get all the users away statuses
|
||||||
|
if (isSelf) {
|
||||||
|
performWhoOnChannel(chan);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
40
src/plugins/irc-events/users.ts
Normal file
40
src/plugins/irc-events/users.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import type {IrcEventHandler} from "../../client";
|
||||||
|
import {ChanType} from "../../models/chan";
|
||||||
|
|
||||||
|
export default <IrcEventHandler>function (irc, network) {
|
||||||
|
const client = this;
|
||||||
|
|
||||||
|
irc.on("users online", function (data) {
|
||||||
|
const changedChannels: string[] = [];
|
||||||
|
|
||||||
|
for (const nick of data.nicks) {
|
||||||
|
for (const channel of network.channels) {
|
||||||
|
if (channel.type === ChanType.QUERY && channel.name === nick) {
|
||||||
|
channel.isOnline = true;
|
||||||
|
changedChannels.push(channel.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.emit("users:online", {changedChannels, networkId: network.uuid});
|
||||||
|
});
|
||||||
|
|
||||||
|
irc.on("users offline", function (data) {
|
||||||
|
const changedChannels: string[] = [];
|
||||||
|
|
||||||
|
for (const nick of data.nicks) {
|
||||||
|
for (const channel of network.channels) {
|
||||||
|
if (channel.type === ChanType.QUERY && channel.name === nick) {
|
||||||
|
channel.isOnline = false;
|
||||||
|
changedChannels.push(channel.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.emit("users:offline", {changedChannels, networkId: network.uuid});
|
||||||
|
});
|
||||||
|
};
|
7
src/types/modules/irc-framework.d.ts
vendored
7
src/types/modules/irc-framework.d.ts
vendored
|
@ -37,8 +37,7 @@ declare module "irc-framework" {
|
||||||
type: "privmsg" | "action"; // TODO
|
type: "privmsg" | "action"; // TODO
|
||||||
}
|
}
|
||||||
export interface JoinEventArgs {
|
export interface JoinEventArgs {
|
||||||
// todo: is that wrong?
|
account: string;
|
||||||
account: boolean;
|
|
||||||
channel: string;
|
channel: string;
|
||||||
gecos: string;
|
gecos: string;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
|
@ -235,6 +234,10 @@ declare module "irc-framework" {
|
||||||
|
|
||||||
matchAction(match_regex: string, cb: (event: Event) => any): void;
|
matchAction(match_regex: string, cb: (event: Event) => any): void;
|
||||||
|
|
||||||
|
addMonitor(target: string): void;
|
||||||
|
|
||||||
|
removeMonitor(target: string): void;
|
||||||
|
|
||||||
stringToBlocks(str: string, block_size?: number): string[];
|
stringToBlocks(str: string, block_size?: number): string[];
|
||||||
|
|
||||||
on(eventType: string | symbol, cb: (event: any) => void): this;
|
on(eventType: string | symbol, cb: (event: any) => void): this;
|
||||||
|
|
4
src/types/socket-events.d.ts
vendored
4
src/types/socket-events.d.ts
vendored
|
@ -120,6 +120,10 @@ interface ServerToClientEvents {
|
||||||
network: string;
|
network: string;
|
||||||
chan: InitClientChan;
|
chan: InitClientChan;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
|
||||||
|
"users:online": (args: {changedChannels: string[]; networkId: string}) => void;
|
||||||
|
|
||||||
|
"users:offline": (args: {changedChannels: string[]; networkId: string}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClientToServerEvents {
|
interface ClientToServerEvents {
|
||||||
|
|
Loading…
Reference in a new issue