components: Drop telepathy client
The telepathy client component has been unmaintained for a long time. Now that we rework the notifications massively it's time to drop the support for it. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3173>
This commit is contained in:
parent
d1cf01d67c
commit
c5ec3e45e4
@ -23,42 +23,3 @@ $notification_banner_width: 34em;
|
||||
@extend %bubble_button;
|
||||
}
|
||||
}
|
||||
|
||||
// counter
|
||||
.summary-source-counter {
|
||||
font-size: $base_font_size - 1pt;
|
||||
font-weight: bold;
|
||||
height: 1.6em;
|
||||
width: 1.6em;
|
||||
-shell-counter-overlap-x: 3px;
|
||||
-shell-counter-overlap-y: 3px;
|
||||
background-color: $selected_bg_color;
|
||||
color: $selected_fg_color;
|
||||
border: 2px solid $fg_color;
|
||||
box-shadow: 0 2px 2px rgba(0,0,0,0.5);
|
||||
border-radius: 0.9em; // should be 0.8 but whatever; wish I could do 50%;
|
||||
}
|
||||
|
||||
// chat bubbles
|
||||
.chat-body { spacing: 5px; }
|
||||
.chat-response { margin: 5px; }
|
||||
.chat-log-message { color: darken($fg_color,10%); }
|
||||
.chat-new-group { padding-top: 1em; }
|
||||
.chat-received {
|
||||
padding-left: 4px;
|
||||
&:rtl { padding-left: 0px; padding-right: 4px; }
|
||||
}
|
||||
|
||||
.chat-sent {
|
||||
padding-left: 18pt;
|
||||
color: lighten($fg_color, 15%);
|
||||
&:rtl { padding-left: 0; padding-right: 18pt; }
|
||||
}
|
||||
|
||||
.chat-meta-message {
|
||||
@extend %caption;
|
||||
padding-left: 4px;
|
||||
font-weight: bold;
|
||||
color: lighten($fg_color,18%);
|
||||
&:rtl { padding-left: 0; padding-right: 4px; }
|
||||
}
|
||||
|
@ -135,7 +135,6 @@
|
||||
<file>ui/components/keyring.js</file>
|
||||
<file>ui/components/networkAgent.js</file>
|
||||
<file>ui/components/polkitAgent.js</file>
|
||||
<file>ui/components/telepathyClient.js</file>
|
||||
|
||||
<file>ui/status/accessibility.js</file>
|
||||
<file>ui/status/autoRotate.js</file>
|
||||
|
@ -63,12 +63,3 @@ try {
|
||||
} catch {
|
||||
console.debug('Malcontent is not available, parental controls integration will be disabled.');
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Telepathy is optional, so catch any errors loading it
|
||||
gi.require('TelepathyGLib', '0.12');
|
||||
gi.require('TelepathyLogger', '0.2');
|
||||
} catch {
|
||||
console.debug('Telepathy is not available, chat integration will be disabled.');
|
||||
}
|
||||
|
@ -1,982 +0,0 @@
|
||||
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||||
import Clutter from 'gi://Clutter';
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
import St from 'gi://St';
|
||||
|
||||
let Tpl = null;
|
||||
let Tp = null;
|
||||
try {
|
||||
({default: Tp} = await import('gi://TelepathyGlib'));
|
||||
({default: Tpl} = await import('gi://TelepathyLogger'));
|
||||
|
||||
Gio._promisify(Tp.Channel.prototype, 'close_async');
|
||||
Gio._promisify(Tp.TextChannel.prototype, 'send_message_async');
|
||||
Gio._promisify(Tp.ChannelDispatchOperation.prototype, 'claim_with_async');
|
||||
Gio._promisify(Tpl.LogManager.prototype, 'get_filtered_events_async');
|
||||
} catch {
|
||||
console.debug('Skipping chat support, telepathy not found');
|
||||
}
|
||||
|
||||
import * as History from '../../misc/history.js';
|
||||
import * as Main from '../main.js';
|
||||
import * as MessageList from '../messageList.js';
|
||||
import * as MessageTray from '../messageTray.js';
|
||||
import * as Params from '../../misc/params.js';
|
||||
import * as Util from '../../misc/util.js';
|
||||
|
||||
const HAVE_TP = Tp != null && Tpl != null;
|
||||
|
||||
// See Notification.appendMessage
|
||||
const SCROLLBACK_IMMEDIATE_TIME = 3 * 60; // 3 minutes
|
||||
const SCROLLBACK_RECENT_TIME = 15 * 60; // 15 minutes
|
||||
const SCROLLBACK_RECENT_LENGTH = 20;
|
||||
const SCROLLBACK_IDLE_LENGTH = 5;
|
||||
|
||||
// See Source._displayPendingMessages
|
||||
const SCROLLBACK_HISTORY_LINES = 10;
|
||||
|
||||
// See Notification._onEntryChanged
|
||||
const COMPOSING_STOP_TIMEOUT = 5;
|
||||
|
||||
const CHAT_EXPAND_LINES = 12;
|
||||
|
||||
const NotificationDirection = {
|
||||
SENT: 'chat-sent',
|
||||
RECEIVED: 'chat-received',
|
||||
};
|
||||
|
||||
const ChatMessage = HAVE_TP ? GObject.registerClass({
|
||||
Properties: {
|
||||
'message-type': GObject.ParamSpec.int(
|
||||
'message-type', 'message-type', 'message-type',
|
||||
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
|
||||
Math.min(...Object.values(Tp.ChannelTextMessageType)),
|
||||
Math.max(...Object.values(Tp.ChannelTextMessageType)),
|
||||
Tp.ChannelTextMessageType.NORMAL),
|
||||
'text': GObject.ParamSpec.string(
|
||||
'text', 'text', 'text',
|
||||
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
|
||||
null),
|
||||
'sender': GObject.ParamSpec.string(
|
||||
'sender', 'sender', 'sender',
|
||||
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
|
||||
null),
|
||||
'timestamp': GObject.ParamSpec.int64(
|
||||
'timestamp', 'timestamp', 'timestamp',
|
||||
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
|
||||
0, Number.MAX_SAFE_INTEGER, 0),
|
||||
'direction': GObject.ParamSpec.string(
|
||||
'direction', 'direction', 'direction',
|
||||
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
|
||||
null),
|
||||
},
|
||||
}, class ChatMessageClass extends GObject.Object {
|
||||
static newFromTpMessage(tpMessage, direction) {
|
||||
return new ChatMessage({
|
||||
'message-type': tpMessage.get_message_type(),
|
||||
'text': tpMessage.to_text()[0],
|
||||
'sender': tpMessage.sender.alias,
|
||||
'timestamp': direction === NotificationDirection.RECEIVED
|
||||
? tpMessage.get_received_timestamp() : tpMessage.get_sent_timestamp(),
|
||||
direction,
|
||||
});
|
||||
}
|
||||
|
||||
static newFromTplTextEvent(tplTextEvent) {
|
||||
let direction =
|
||||
tplTextEvent.get_sender().get_entity_type() === Tpl.EntityType.SELF
|
||||
? NotificationDirection.SENT : NotificationDirection.RECEIVED;
|
||||
|
||||
return new ChatMessage({
|
||||
'message-type': tplTextEvent.get_message_type(),
|
||||
'text': tplTextEvent.get_message(),
|
||||
'sender': tplTextEvent.get_sender().get_alias(),
|
||||
'timestamp': tplTextEvent.get_timestamp(),
|
||||
direction,
|
||||
});
|
||||
}
|
||||
}) : null;
|
||||
|
||||
|
||||
class TelepathyComponent {
|
||||
constructor() {
|
||||
this._client = null;
|
||||
|
||||
if (!HAVE_TP)
|
||||
return; // Telepathy isn't available
|
||||
|
||||
this._client = new TelepathyClient();
|
||||
}
|
||||
|
||||
enable() {
|
||||
if (!this._client)
|
||||
return;
|
||||
|
||||
try {
|
||||
this._client.register();
|
||||
} catch (e) {
|
||||
throw new Error(`Could not register Telepathy client. Error: ${e}`);
|
||||
}
|
||||
|
||||
if (!this._client.account_manager.is_prepared(Tp.AccountManager.get_feature_quark_core()))
|
||||
this._client.account_manager.prepare_async(null, null);
|
||||
}
|
||||
|
||||
disable() {
|
||||
if (!this._client)
|
||||
return;
|
||||
|
||||
this._client.unregister();
|
||||
}
|
||||
}
|
||||
|
||||
const TelepathyClient = HAVE_TP ? GObject.registerClass(
|
||||
class TelepathyClient extends Tp.BaseClient {
|
||||
_init() {
|
||||
// channel path -> ChatSource
|
||||
this._chatSources = {};
|
||||
this._chatState = Tp.ChannelChatState.ACTIVE;
|
||||
|
||||
// account path -> AccountNotification
|
||||
this._accountNotifications = {};
|
||||
|
||||
// Define features we want
|
||||
this._accountManager = Tp.AccountManager.dup();
|
||||
let factory = this._accountManager.get_factory();
|
||||
factory.add_account_features([Tp.Account.get_feature_quark_connection()]);
|
||||
factory.add_connection_features([Tp.Connection.get_feature_quark_contact_list()]);
|
||||
factory.add_channel_features([Tp.Channel.get_feature_quark_contacts()]);
|
||||
factory.add_contact_features([
|
||||
Tp.ContactFeature.ALIAS,
|
||||
Tp.ContactFeature.AVATAR_DATA,
|
||||
Tp.ContactFeature.PRESENCE,
|
||||
Tp.ContactFeature.SUBSCRIPTION_STATES,
|
||||
]);
|
||||
|
||||
// Set up a SimpleObserver, which will call _observeChannels whenever a
|
||||
// channel matching its filters is detected.
|
||||
// The second argument, recover, means _observeChannels will be run
|
||||
// for any existing channel as well.
|
||||
super._init({
|
||||
name: 'GnomeShell',
|
||||
account_manager: this._accountManager,
|
||||
uniquify_name: true,
|
||||
});
|
||||
|
||||
// We only care about single-user text-based chats
|
||||
let filter = {};
|
||||
filter[Tp.PROP_CHANNEL_CHANNEL_TYPE] = Tp.IFACE_CHANNEL_TYPE_TEXT;
|
||||
filter[Tp.PROP_CHANNEL_TARGET_HANDLE_TYPE] = Tp.HandleType.CONTACT;
|
||||
|
||||
this.set_observer_recover(true);
|
||||
this.add_observer_filter(filter);
|
||||
this.add_approver_filter(filter);
|
||||
this.add_handler_filter(filter);
|
||||
|
||||
// Allow other clients (such as Empathy) to preempt our channels if
|
||||
// needed
|
||||
this.set_delegated_channels_callback(
|
||||
this._delegatedChannelsCb.bind(this));
|
||||
}
|
||||
|
||||
vfunc_observe_channels(...args) {
|
||||
let [account, conn, channels, dispatchOp_, requests_, context] = args;
|
||||
let len = channels.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
let channel = channels[i];
|
||||
let [targetHandle_, targetHandleType] = channel.get_handle();
|
||||
|
||||
if (channel.get_invalidated())
|
||||
continue;
|
||||
|
||||
/* Only observe contact text channels */
|
||||
if (!(channel instanceof Tp.TextChannel) ||
|
||||
targetHandleType !== Tp.HandleType.CONTACT)
|
||||
continue;
|
||||
|
||||
this._createChatSource(account, conn, channel, channel.get_target_contact());
|
||||
}
|
||||
|
||||
context.accept();
|
||||
}
|
||||
|
||||
_createChatSource(account, conn, channel, contact) {
|
||||
if (this._chatSources[channel.get_object_path()])
|
||||
return;
|
||||
|
||||
let source = new ChatSource(account, conn, channel, contact, this);
|
||||
|
||||
this._chatSources[channel.get_object_path()] = source;
|
||||
source.connect('destroy', () => {
|
||||
delete this._chatSources[channel.get_object_path()];
|
||||
});
|
||||
}
|
||||
|
||||
vfunc_handle_channels(...args) {
|
||||
let [account, conn, channels, requests_, userActionTime_, context] = args;
|
||||
this._handlingChannels(account, conn, channels, true);
|
||||
context.accept();
|
||||
}
|
||||
|
||||
_handlingChannels(account, conn, channels, notify) {
|
||||
let len = channels.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
let channel = channels[i];
|
||||
|
||||
// We can only handle text channel, so close any other channel
|
||||
if (!(channel instanceof Tp.TextChannel)) {
|
||||
channel.close_async();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (channel.get_invalidated())
|
||||
continue;
|
||||
|
||||
// 'notify' will be true when coming from an actual HandleChannels
|
||||
// call, and not when from a successful Claim call. The point is
|
||||
// we don't want to notify for a channel we just claimed which
|
||||
// has no new messages (for example, a new channel which only has
|
||||
// a delivery notification). We rely on _displayPendingMessages()
|
||||
// and _messageReceived() to notify for new messages.
|
||||
|
||||
// But we should still notify from HandleChannels because the
|
||||
// Telepathy spec states that handlers must foreground channels
|
||||
// in HandleChannels calls which are already being handled.
|
||||
|
||||
if (notify && this.is_handling_channel(channel)) {
|
||||
// We are already handling the channel, display the source
|
||||
let source = this._chatSources[channel.get_object_path()];
|
||||
if (source)
|
||||
source.showNotification();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vfunc_add_dispatch_operation(...args) {
|
||||
let [account, conn, channels, dispatchOp, context] = args;
|
||||
let channel = channels[0];
|
||||
let chanType = channel.get_channel_type();
|
||||
|
||||
if (channel.get_invalidated()) {
|
||||
context.fail(new Tp.Error({
|
||||
code: Tp.Error.INVALID_ARGUMENT,
|
||||
message: 'Channel is invalidated',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (chanType === Tp.IFACE_CHANNEL_TYPE_TEXT) {
|
||||
this._approveTextChannel(account, conn, channel, dispatchOp, context);
|
||||
} else {
|
||||
context.fail(new Tp.Error({
|
||||
code: Tp.Error.INVALID_ARGUMENT,
|
||||
message: 'Unsupported channel type',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async _approveTextChannel(account, conn, channel, dispatchOp, context) {
|
||||
let [targetHandle_, targetHandleType] = channel.get_handle();
|
||||
|
||||
if (targetHandleType !== Tp.HandleType.CONTACT) {
|
||||
context.fail(new Tp.Error({
|
||||
code: Tp.Error.INVALID_ARGUMENT,
|
||||
message: 'Unsupported handle type',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
context.accept();
|
||||
|
||||
// Approve private text channels right away as we are going to handle it
|
||||
try {
|
||||
await dispatchOp.claim_with_async(this);
|
||||
this._handlingChannels(account, conn, [channel], false);
|
||||
} catch (err) {
|
||||
log(`Failed to claim channel: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
_delegatedChannelsCb(_client, _channels) {
|
||||
// Nothing to do as we don't make a distinction between observed and
|
||||
// handled channels.
|
||||
}
|
||||
}) : null;
|
||||
|
||||
const ChatSource = HAVE_TP ? GObject.registerClass(
|
||||
class ChatSource extends MessageTray.Source {
|
||||
constructor(account, conn, channel, contact, client) {
|
||||
const appId = account.protocol_name === 'irc'
|
||||
? 'org.gnome.Polari'
|
||||
: 'empathy';
|
||||
const policy =
|
||||
new MessageTray.NotificationApplicationPolicy(appId);
|
||||
|
||||
super({policy});
|
||||
|
||||
this._account = account;
|
||||
this._contact = contact;
|
||||
this._client = client;
|
||||
|
||||
this._pendingMessages = [];
|
||||
|
||||
this._conn = conn;
|
||||
this._channel = channel;
|
||||
|
||||
this._notifyTimeoutId = 0;
|
||||
|
||||
this._presence = contact.get_presence_type();
|
||||
|
||||
this._channel.connectObject(
|
||||
'invalidated', this._channelClosed.bind(this),
|
||||
'message-sent', this._messageSent.bind(this),
|
||||
'message-received', this._messageReceived.bind(this),
|
||||
'pending-message-removed', this._pendingRemoved.bind(this), this);
|
||||
|
||||
this._contact.connectObject(
|
||||
'notify::alias', this._updateAlias.bind(this),
|
||||
'notify::avatar-file', this._updateAvatarIcon.bind(this), this);
|
||||
|
||||
this._updateAlias();
|
||||
this._updateAvatarIcon();
|
||||
// Add ourselves as a source.
|
||||
Main.messageTray.add(this);
|
||||
|
||||
this._getLogMessages();
|
||||
}
|
||||
|
||||
_ensureNotification() {
|
||||
if (this._notification)
|
||||
return;
|
||||
|
||||
this._notification = new ChatNotification(this);
|
||||
this._notification.connectObject(
|
||||
'activated', this.open.bind(this),
|
||||
'destroy', () => (this._notification = null),
|
||||
'updated', () => {
|
||||
if (this._banner && this._banner.expanded)
|
||||
this._ackMessages();
|
||||
}, this);
|
||||
this.pushNotification(this._notification);
|
||||
}
|
||||
|
||||
createBanner() {
|
||||
this._banner = new ChatNotificationBanner(this._notification);
|
||||
|
||||
// We ack messages when the user expands the new notification
|
||||
this._banner.connectObject(
|
||||
'expanded', this._ackMessages.bind(this),
|
||||
'destroy', () => (this._banner = null), this);
|
||||
|
||||
return this._banner;
|
||||
}
|
||||
|
||||
_updateAlias() {
|
||||
let oldAlias = this.title;
|
||||
let newAlias = this._contact.get_alias();
|
||||
|
||||
if (oldAlias === newAlias)
|
||||
return;
|
||||
|
||||
this.title = newAlias;
|
||||
if (this._notification)
|
||||
this._notification.appendAliasChange(oldAlias, newAlias);
|
||||
}
|
||||
|
||||
_updateAvatarIcon() {
|
||||
let file = this._contact.get_avatar_file();
|
||||
if (file) {
|
||||
if (this.icon instanceof Gio.FileIcon)
|
||||
this.icon.file = file;
|
||||
else
|
||||
this.icon = new Gio.FileIcon({file});
|
||||
} else if (!(this.icon instanceof Gio.ThemedIcon)) {
|
||||
this.iconName = 'avatar-default';
|
||||
}
|
||||
|
||||
if (this._notification && this.icon !== this._notification.gicon) {
|
||||
this._notification.update(
|
||||
this._notification.title,
|
||||
this._notification.bannerBodyText,
|
||||
{gicon: this.icon});
|
||||
}
|
||||
}
|
||||
|
||||
open() {
|
||||
Main.overview.hide();
|
||||
Main.panel.closeCalendar();
|
||||
|
||||
if (this._client.is_handling_channel(this._channel)) {
|
||||
// We are handling the channel, try to pass it to Empathy or Polari
|
||||
// (depending on the channel type)
|
||||
// We don't check if either app is available - mission control will
|
||||
// fallback to something else if activation fails
|
||||
|
||||
let target;
|
||||
if (this._channel.connection.protocol_name === 'irc')
|
||||
target = 'org.freedesktop.Telepathy.Client.Polari';
|
||||
else
|
||||
target = 'org.freedesktop.Telepathy.Client.Empathy.Chat';
|
||||
this._client.delegate_channels_async([this._channel], global.get_current_time(), target, null);
|
||||
} else {
|
||||
// We are not the handler, just ask to present the channel
|
||||
let dbus = Tp.DBusDaemon.dup();
|
||||
let cd = Tp.ChannelDispatcher.new(dbus);
|
||||
|
||||
cd.present_channel_async(this._channel, global.get_current_time(), null);
|
||||
}
|
||||
}
|
||||
|
||||
async _getLogMessages() {
|
||||
let logManager = Tpl.LogManager.dup_singleton();
|
||||
let entity = Tpl.Entity.new_from_tp_contact(this._contact, Tpl.EntityType.CONTACT);
|
||||
|
||||
const [events] = await logManager.get_filtered_events_async(
|
||||
this._account, entity,
|
||||
Tpl.EventTypeMask.TEXT, SCROLLBACK_HISTORY_LINES,
|
||||
null);
|
||||
|
||||
let logMessages = events.map(e => ChatMessage.newFromTplTextEvent(e));
|
||||
this._ensureNotification();
|
||||
|
||||
let pendingTpMessages = this._channel.get_pending_messages();
|
||||
let pendingMessages = [];
|
||||
|
||||
for (let i = 0; i < pendingTpMessages.length; i++) {
|
||||
let message = pendingTpMessages[i];
|
||||
|
||||
if (message.get_message_type() === Tp.ChannelTextMessageType.DELIVERY_REPORT)
|
||||
continue;
|
||||
|
||||
pendingMessages.push(ChatMessage.newFromTpMessage(message,
|
||||
NotificationDirection.RECEIVED));
|
||||
|
||||
this._pendingMessages.push(message);
|
||||
}
|
||||
|
||||
this.countUpdated();
|
||||
|
||||
let showTimestamp = false;
|
||||
|
||||
for (let i = 0; i < logMessages.length; i++) {
|
||||
let logMessage = logMessages[i];
|
||||
let isPending = false;
|
||||
|
||||
// Skip any log messages that are also in pendingMessages
|
||||
for (let j = 0; j < pendingMessages.length; j++) {
|
||||
let pending = pendingMessages[j];
|
||||
if (logMessage.timestamp === pending.timestamp && logMessage.text === pending.text) {
|
||||
isPending = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPending) {
|
||||
showTimestamp = true;
|
||||
this._notification.appendMessage(logMessage, true, ['chat-log-message']);
|
||||
}
|
||||
}
|
||||
|
||||
if (showTimestamp)
|
||||
this._notification.appendTimestamp();
|
||||
|
||||
for (let i = 0; i < pendingMessages.length; i++)
|
||||
this._notification.appendMessage(pendingMessages[i], true);
|
||||
|
||||
if (pendingMessages.length > 0)
|
||||
this.showNotification();
|
||||
}
|
||||
|
||||
destroy(reason) {
|
||||
if (this._client.is_handling_channel(this._channel)) {
|
||||
this._ackMessages();
|
||||
// The chat box has been destroyed so it can't
|
||||
// handle the channel any more.
|
||||
this._channel.close_async();
|
||||
} else {
|
||||
// Don't indicate any unread messages when the notification
|
||||
// that represents them has been destroyed.
|
||||
this._pendingMessages = [];
|
||||
this.countUpdated();
|
||||
}
|
||||
|
||||
// Keep source alive while the channel is open
|
||||
if (reason !== MessageTray.NotificationDestroyedReason.SOURCE_CLOSED)
|
||||
return;
|
||||
|
||||
if (this._destroyed)
|
||||
return;
|
||||
|
||||
this._destroyed = true;
|
||||
this._channel.disconnectObject(this);
|
||||
this._contact.disconnectObject(this);
|
||||
|
||||
super.destroy(reason);
|
||||
}
|
||||
|
||||
_channelClosed() {
|
||||
this.destroy(MessageTray.NotificationDestroyedReason.SOURCE_CLOSED);
|
||||
}
|
||||
|
||||
/* All messages are new messages for Telepathy sources */
|
||||
get count() {
|
||||
return this._pendingMessages.length;
|
||||
}
|
||||
|
||||
get unseenCount() {
|
||||
return this.count;
|
||||
}
|
||||
|
||||
get countVisible() {
|
||||
return this.count > 0;
|
||||
}
|
||||
|
||||
_messageReceived(channel, message) {
|
||||
if (message.get_message_type() === Tp.ChannelTextMessageType.DELIVERY_REPORT)
|
||||
return;
|
||||
|
||||
this._ensureNotification();
|
||||
this._pendingMessages.push(message);
|
||||
this.countUpdated();
|
||||
|
||||
message = ChatMessage.newFromTpMessage(message,
|
||||
NotificationDirection.RECEIVED);
|
||||
this._notification.appendMessage(message);
|
||||
|
||||
// Wait a bit before notifying for the received message, a handler
|
||||
// could ack it in the meantime.
|
||||
if (this._notifyTimeoutId !== 0)
|
||||
GLib.source_remove(this._notifyTimeoutId);
|
||||
this._notifyTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500,
|
||||
this._notifyTimeout.bind(this));
|
||||
GLib.Source.set_name_by_id(this._notifyTimeoutId, '[gnome-shell] this._notifyTimeout');
|
||||
}
|
||||
|
||||
_notifyTimeout() {
|
||||
if (this._pendingMessages.length !== 0)
|
||||
this.showNotification();
|
||||
|
||||
this._notifyTimeoutId = 0;
|
||||
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
// This is called for both messages we send from
|
||||
// our client and other clients as well.
|
||||
_messageSent(channel, message, _flags, _token) {
|
||||
this._ensureNotification();
|
||||
message = ChatMessage.newFromTpMessage(message,
|
||||
NotificationDirection.SENT);
|
||||
this._notification.appendMessage(message);
|
||||
}
|
||||
|
||||
showNotification() {
|
||||
super.showNotification(this._notification);
|
||||
}
|
||||
|
||||
respond(text) {
|
||||
let type;
|
||||
if (text.slice(0, 4) === '/me ') {
|
||||
type = Tp.ChannelTextMessageType.ACTION;
|
||||
text = text.slice(4);
|
||||
} else {
|
||||
type = Tp.ChannelTextMessageType.NORMAL;
|
||||
}
|
||||
|
||||
let msg = Tp.ClientMessage.new_text(type, text);
|
||||
this._channel.send_message_async(msg, 0);
|
||||
}
|
||||
|
||||
setChatState(state) {
|
||||
// We don't want to send COMPOSING every time a letter is typed into
|
||||
// the entry. We send the state only when it changes. Telepathy/Empathy
|
||||
// might change it behind our back if the user is using both
|
||||
// gnome-shell's entry and the Empathy conversation window. We could
|
||||
// keep track of it with the ChatStateChanged signal but it is good
|
||||
// enough right now.
|
||||
if (state !== this._chatState) {
|
||||
this._chatState = state;
|
||||
this._channel.set_chat_state_async(state, null);
|
||||
}
|
||||
}
|
||||
|
||||
_pendingRemoved(channel, message) {
|
||||
let idx = this._pendingMessages.indexOf(message);
|
||||
|
||||
if (idx >= 0) {
|
||||
this._pendingMessages.splice(idx, 1);
|
||||
this.countUpdated();
|
||||
}
|
||||
|
||||
if (this._pendingMessages.length === 0 &&
|
||||
this._banner && !this._banner.expanded)
|
||||
this._banner.hide();
|
||||
}
|
||||
|
||||
_ackMessages() {
|
||||
// Don't clear our messages here, tp-glib will send a
|
||||
// 'pending-message-removed' for each one.
|
||||
this._channel.ack_all_pending_messages_async(null);
|
||||
}
|
||||
}) : null;
|
||||
|
||||
const ChatNotificationMessage = HAVE_TP ? GObject.registerClass(
|
||||
class ChatNotificationMessage extends GObject.Object {
|
||||
_init(props = {}) {
|
||||
super._init();
|
||||
this.set(props);
|
||||
}
|
||||
}) : null;
|
||||
|
||||
const ChatNotification = HAVE_TP ? GObject.registerClass({
|
||||
Signals: {
|
||||
'message-removed': {param_types: [ChatNotificationMessage.$gtype]},
|
||||
'message-added': {param_types: [ChatNotificationMessage.$gtype]},
|
||||
'timestamp-changed': {param_types: [ChatNotificationMessage.$gtype]},
|
||||
},
|
||||
}, class ChatNotification extends MessageTray.Notification {
|
||||
constructor(source) {
|
||||
super(source, source.title, null, null);
|
||||
this.setUrgency(MessageTray.Urgency.HIGH);
|
||||
this.setResident(true);
|
||||
|
||||
this.messages = [];
|
||||
this._timestampTimeoutId = 0;
|
||||
}
|
||||
|
||||
destroy(reason) {
|
||||
if (this._timestampTimeoutId)
|
||||
GLib.source_remove(this._timestampTimeoutId);
|
||||
this._timestampTimeoutId = 0;
|
||||
super.destroy(reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} message - An object with the properties
|
||||
* {string} message.text - the body of the message,
|
||||
* {Tp.ChannelTextMessageType} message.messageType - the type
|
||||
* {string} message.sender - the name of the sender,
|
||||
* {number} message.timestamp - the time the message was sent
|
||||
* {NotificationDirection} message.direction - a #NotificationDirection
|
||||
*
|
||||
* @param {bool} noTimestamp - Whether to add a timestamp. If %true,
|
||||
* no timestamp will be added, regardless of the difference since
|
||||
* the last timestamp
|
||||
*/
|
||||
appendMessage(message, noTimestamp) {
|
||||
let messageBody = GLib.markup_escape_text(message.text, -1);
|
||||
let styles = [message.direction];
|
||||
|
||||
if (message.messageType === Tp.ChannelTextMessageType.ACTION) {
|
||||
let senderAlias = GLib.markup_escape_text(message.sender, -1);
|
||||
messageBody = `<i>${senderAlias}</i> ${messageBody}`;
|
||||
styles.push('chat-action');
|
||||
}
|
||||
|
||||
if (message.direction === NotificationDirection.RECEIVED) {
|
||||
this.update(this.source.title, messageBody, {
|
||||
datetime: GLib.DateTime.new_from_unix_local(message.timestamp),
|
||||
bannerMarkup: true,
|
||||
});
|
||||
}
|
||||
|
||||
let group = message.direction === NotificationDirection.RECEIVED
|
||||
? 'received' : 'sent';
|
||||
|
||||
this._append({
|
||||
body: messageBody,
|
||||
group,
|
||||
styles,
|
||||
timestamp: message.timestamp,
|
||||
noTimestamp,
|
||||
});
|
||||
}
|
||||
|
||||
_filterMessages() {
|
||||
if (this.messages.length < 1)
|
||||
return;
|
||||
|
||||
let lastMessageTime = this.messages[0].timestamp;
|
||||
let currentTime = Date.now() / 1000;
|
||||
|
||||
// Keep the scrollback from growing too long. If the most
|
||||
// recent message (before the one we just added) is within
|
||||
// SCROLLBACK_RECENT_TIME, we will keep
|
||||
// SCROLLBACK_RECENT_LENGTH previous messages. Otherwise
|
||||
// we'll keep SCROLLBACK_IDLE_LENGTH messages.
|
||||
|
||||
let maxLength = lastMessageTime < currentTime - SCROLLBACK_RECENT_TIME
|
||||
? SCROLLBACK_IDLE_LENGTH : SCROLLBACK_RECENT_LENGTH;
|
||||
|
||||
let filteredHistory = this.messages.filter(item => item.realMessage);
|
||||
if (filteredHistory.length > maxLength) {
|
||||
let lastMessageToKeep = filteredHistory[maxLength];
|
||||
let expired = this.messages.splice(this.messages.indexOf(lastMessageToKeep));
|
||||
for (let i = 0; i < expired.length; i++)
|
||||
this.emit('message-removed', expired[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} props - An object with the properties:
|
||||
* {string} props.body - The text of the message.
|
||||
* {string} props.group - The group of the message, one of:
|
||||
* 'received', 'sent', 'meta'.
|
||||
* {string[]} props.styles - Style class names for the message to have.
|
||||
* {number} props.timestamp - The timestamp of the message.
|
||||
* {bool} props.noTimestamp - suppress timestamp signal?
|
||||
*/
|
||||
_append(props) {
|
||||
let currentTime = Date.now() / 1000;
|
||||
props = Params.parse(props, {
|
||||
body: null,
|
||||
group: null,
|
||||
styles: [],
|
||||
timestamp: currentTime,
|
||||
noTimestamp: false,
|
||||
});
|
||||
const {noTimestamp} = props;
|
||||
delete props.noTimestamp;
|
||||
|
||||
// Reset the old message timeout
|
||||
if (this._timestampTimeoutId)
|
||||
GLib.source_remove(this._timestampTimeoutId);
|
||||
this._timestampTimeoutId = 0;
|
||||
|
||||
let message = new ChatNotificationMessage({
|
||||
realMessage: props.group !== 'meta',
|
||||
showTimestamp: false,
|
||||
...props,
|
||||
});
|
||||
|
||||
this.messages.unshift(message);
|
||||
this.emit('message-added', message);
|
||||
|
||||
if (!noTimestamp) {
|
||||
let timestamp = props.timestamp;
|
||||
if (timestamp < currentTime - SCROLLBACK_IMMEDIATE_TIME) {
|
||||
this.appendTimestamp();
|
||||
} else {
|
||||
// Schedule a new timestamp in SCROLLBACK_IMMEDIATE_TIME
|
||||
// from the timestamp of the message.
|
||||
this._timestampTimeoutId = GLib.timeout_add_seconds(
|
||||
GLib.PRIORITY_DEFAULT,
|
||||
SCROLLBACK_IMMEDIATE_TIME - (currentTime - timestamp),
|
||||
this.appendTimestamp.bind(this));
|
||||
GLib.Source.set_name_by_id(this._timestampTimeoutId, '[gnome-shell] this.appendTimestamp');
|
||||
}
|
||||
}
|
||||
|
||||
this._filterMessages();
|
||||
}
|
||||
|
||||
appendTimestamp() {
|
||||
this._timestampTimeoutId = 0;
|
||||
|
||||
this.messages[0].showTimestamp = true;
|
||||
this.emit('timestamp-changed', this.messages[0]);
|
||||
|
||||
this._filterMessages();
|
||||
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
appendAliasChange(oldAlias, newAlias) {
|
||||
oldAlias = GLib.markup_escape_text(oldAlias, -1);
|
||||
newAlias = GLib.markup_escape_text(newAlias, -1);
|
||||
|
||||
/* Translators: this is the other person changing their old IM name to their new
|
||||
IM name. */
|
||||
const message = `<i>${
|
||||
_('%s is now known as %s').format(oldAlias, newAlias)}</i>`;
|
||||
|
||||
this._append({
|
||||
body: message,
|
||||
group: 'meta',
|
||||
styles: ['chat-meta-message'],
|
||||
});
|
||||
|
||||
this._filterMessages();
|
||||
}
|
||||
}) : null;
|
||||
|
||||
const ChatLineBox = GObject.registerClass(
|
||||
class ChatLineBox extends St.BoxLayout {
|
||||
vfunc_get_preferred_height(forWidth) {
|
||||
let [, natHeight] = super.vfunc_get_preferred_height(forWidth);
|
||||
return [natHeight, natHeight];
|
||||
}
|
||||
});
|
||||
|
||||
const ChatNotificationBanner = GObject.registerClass(
|
||||
class ChatNotificationBanner extends MessageTray.NotificationBanner {
|
||||
_init(notification) {
|
||||
super._init(notification);
|
||||
|
||||
this._responseEntry = new St.Entry({
|
||||
style_class: 'chat-response',
|
||||
x_expand: true,
|
||||
can_focus: true,
|
||||
});
|
||||
this._responseEntry.clutter_text.connect('activate', this._onEntryActivated.bind(this));
|
||||
this._responseEntry.clutter_text.connect('text-changed', this._onEntryChanged.bind(this));
|
||||
this.setActionArea(this._responseEntry);
|
||||
|
||||
this._responseEntry.clutter_text.connect('key-focus-in', () => {
|
||||
this.focused = true;
|
||||
});
|
||||
this._responseEntry.clutter_text.connect('key-focus-out', () => {
|
||||
this.focused = false;
|
||||
this.emit('unfocused');
|
||||
});
|
||||
|
||||
this._contentArea = new St.BoxLayout({
|
||||
style_class: 'chat-body',
|
||||
vertical: true,
|
||||
});
|
||||
this._scrollArea = new St.ScrollView({
|
||||
style_class: 'chat-scrollview vfade',
|
||||
visible: this.expanded,
|
||||
child: this._contentArea,
|
||||
});
|
||||
|
||||
this.setExpandedBody(this._scrollArea);
|
||||
this.setExpandedLines(CHAT_EXPAND_LINES);
|
||||
|
||||
this._lastGroup = null;
|
||||
|
||||
// Keep track of the bottom position for the current adjustment and
|
||||
// force a scroll to the bottom if things change while we were at the
|
||||
// bottom
|
||||
this._oldMaxScrollValue = this._scrollArea.vadjustment.value;
|
||||
this._scrollArea.vadjustment.connect('changed', adjustment => {
|
||||
if (adjustment.value === this._oldMaxScrollValue)
|
||||
this.scrollTo(St.Side.BOTTOM);
|
||||
this._oldMaxScrollValue = Math.max(adjustment.lower, adjustment.upper - adjustment.page_size);
|
||||
});
|
||||
|
||||
this._inputHistory = new History.HistoryManager({entry: this._responseEntry.clutter_text});
|
||||
|
||||
this._composingTimeoutId = 0;
|
||||
|
||||
this._messageActors = new Map();
|
||||
|
||||
this.notification.connectObject(
|
||||
'timestamp-changed', (n, message) => this._updateTimestamp(message),
|
||||
'message-added', (n, message) => this._addMessage(message),
|
||||
'message-removed', (n, message) => {
|
||||
let actor = this._messageActors.get(message);
|
||||
if (this._messageActors.delete(message))
|
||||
actor.destroy();
|
||||
}, this);
|
||||
|
||||
for (let i = this.notification.messages.length - 1; i >= 0; i--)
|
||||
this._addMessage(this.notification.messages[i]);
|
||||
}
|
||||
|
||||
scrollTo(side) {
|
||||
let adjustment = this._scrollArea.vadjustment;
|
||||
if (side === St.Side.TOP)
|
||||
adjustment.value = adjustment.lower;
|
||||
else if (side === St.Side.BOTTOM)
|
||||
adjustment.value = adjustment.upper;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.emit('done-displaying');
|
||||
}
|
||||
|
||||
_addMessage(message) {
|
||||
let body = new MessageList.URLHighlighter(message.body, true, true);
|
||||
|
||||
let styles = message.styles;
|
||||
for (let i = 0; i < styles.length; i++)
|
||||
body.add_style_class_name(styles[i]);
|
||||
|
||||
let group = message.group;
|
||||
if (group !== this._lastGroup) {
|
||||
this._lastGroup = group;
|
||||
body.add_style_class_name('chat-new-group');
|
||||
}
|
||||
|
||||
let lineBox = new ChatLineBox();
|
||||
lineBox.add_child(body);
|
||||
this._contentArea.add_child(lineBox);
|
||||
this._messageActors.set(message, lineBox);
|
||||
|
||||
this._updateTimestamp(message);
|
||||
}
|
||||
|
||||
_updateTimestamp(message) {
|
||||
let actor = this._messageActors.get(message);
|
||||
if (!actor)
|
||||
return;
|
||||
|
||||
while (actor.get_n_children() > 1)
|
||||
actor.get_child_at_index(1).destroy();
|
||||
|
||||
if (message.showTimestamp) {
|
||||
let lastMessageTime = message.timestamp;
|
||||
let lastMessageDate = new Date(lastMessageTime * 1000);
|
||||
|
||||
let timeLabel = Util.createTimeLabel(lastMessageDate);
|
||||
timeLabel.style_class = 'chat-meta-message';
|
||||
timeLabel.x_expand = timeLabel.y_expand = true;
|
||||
timeLabel.x_align = timeLabel.y_align = Clutter.ActorAlign.END;
|
||||
|
||||
actor.add_child(timeLabel);
|
||||
}
|
||||
}
|
||||
|
||||
_onEntryActivated() {
|
||||
let text = this._responseEntry.get_text();
|
||||
if (text === '')
|
||||
return;
|
||||
|
||||
this._inputHistory.addItem(text);
|
||||
|
||||
// Telepathy sends out the Sent signal for us.
|
||||
// see Source._messageSent
|
||||
this._responseEntry.set_text('');
|
||||
this.notification.source.respond(text);
|
||||
}
|
||||
|
||||
_composingStopTimeout() {
|
||||
this._composingTimeoutId = 0;
|
||||
|
||||
this.notification.source.setChatState(Tp.ChannelChatState.PAUSED);
|
||||
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
_onEntryChanged() {
|
||||
let text = this._responseEntry.get_text();
|
||||
|
||||
// If we're typing, we want to send COMPOSING.
|
||||
// If we empty the entry, we want to send ACTIVE.
|
||||
// If we've stopped typing for COMPOSING_STOP_TIMEOUT
|
||||
// seconds, we want to send PAUSED.
|
||||
|
||||
// Remove composing timeout.
|
||||
if (this._composingTimeoutId > 0) {
|
||||
GLib.source_remove(this._composingTimeoutId);
|
||||
this._composingTimeoutId = 0;
|
||||
}
|
||||
|
||||
if (text !== '') {
|
||||
this.notification.source.setChatState(Tp.ChannelChatState.COMPOSING);
|
||||
|
||||
this._composingTimeoutId = GLib.timeout_add_seconds(
|
||||
GLib.PRIORITY_DEFAULT,
|
||||
COMPOSING_STOP_TIMEOUT,
|
||||
this._composingStopTimeout.bind(this));
|
||||
GLib.Source.set_name_by_id(this._composingTimeoutId, '[gnome-shell] this._composingStopTimeout');
|
||||
} else {
|
||||
this.notification.source.setChatState(Tp.ChannelChatState.ACTIVE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const Component = TelepathyComponent;
|
@ -59,8 +59,7 @@ export const NotificationDestroyedReason = {
|
||||
|
||||
// Message tray has its custom Urgency enumeration. LOW, NORMAL and CRITICAL
|
||||
// urgency values map to the corresponding values for the notifications received
|
||||
// through the notification daemon. HIGH urgency value is used for chats received
|
||||
// through the Telepathy client.
|
||||
// through the notification daemon.
|
||||
/** @enum {number} */
|
||||
export const Urgency = {
|
||||
LOW: 0,
|
||||
@ -529,7 +528,6 @@ SignalTracker.registerDestroyableType(Notification);
|
||||
export const NotificationBanner = GObject.registerClass({
|
||||
Signals: {
|
||||
'done-displaying': {},
|
||||
'unfocused': {},
|
||||
},
|
||||
}, class NotificationBanner extends Calendar.NotificationMessage {
|
||||
_init(notification) {
|
||||
@ -1125,7 +1123,6 @@ export const MessageTray = GObject.registerClass({
|
||||
let expired = (this._userActiveWhileNotificationShown &&
|
||||
this._notificationTimeoutId === 0 &&
|
||||
this._notification.urgency !== Urgency.CRITICAL &&
|
||||
!this._banner.focused &&
|
||||
!this._pointerInNotification) || this._notificationExpired;
|
||||
let mustClose = this._notificationRemoved || !hasNotifications || expired;
|
||||
|
||||
@ -1166,9 +1163,7 @@ export const MessageTray = GObject.registerClass({
|
||||
}
|
||||
|
||||
this._banner = this._notification.createBanner();
|
||||
this._banner.connectObject(
|
||||
'done-displaying', this._escapeTray.bind(this),
|
||||
'unfocused', () => this._updateState(), this);
|
||||
this._banner.connectObject('done-displaying', this._escapeTray.bind(this), this);
|
||||
|
||||
this._bannerBin.add_child(this._banner);
|
||||
|
||||
|
@ -13,7 +13,7 @@ import * as Config from '../misc/config.js';
|
||||
const DEFAULT_MODE = 'restrictive';
|
||||
|
||||
const USER_SESSION_COMPONENTS = [
|
||||
'polkitAgent', 'telepathyClient', 'keyring',
|
||||
'polkitAgent', 'keyring',
|
||||
'autorunManager', 'automountManager',
|
||||
];
|
||||
|
||||
@ -69,7 +69,7 @@ const _modes = {
|
||||
'unlock-dialog': {
|
||||
isLocked: true,
|
||||
unlockDialog: undefined,
|
||||
components: ['polkitAgent', 'telepathyClient'],
|
||||
components: ['polkitAgent'],
|
||||
panel: {
|
||||
left: [],
|
||||
center: [],
|
||||
|
@ -28,7 +28,6 @@ js/ui/components/autorunManager.js
|
||||
js/ui/components/keyring.js
|
||||
js/ui/components/networkAgent.js
|
||||
js/ui/components/polkitAgent.js
|
||||
js/ui/components/telepathyClient.js
|
||||
js/ui/ctrlAltTab.js
|
||||
js/ui/dash.js
|
||||
js/ui/dateMenu.js
|
||||
|
Loading…
x
Reference in New Issue
Block a user