[polari/wip/kunaaljain/squashed-branch: 1/2] Working Prototype
- From: Kunal Jain <kunaljain src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [polari/wip/kunaaljain/squashed-branch: 1/2] Working Prototype
- Date: Fri, 26 Aug 2016 16:32:14 +0000 (UTC)
commit c43912e7e5d62e810a06130a3de00ce05c4080b2
Author: Kunaal Jain <kunaalus gmail com>
Date: Sun Jun 5 20:26:35 2016 +0530
Working Prototype
data/org.gnome.Polari.data.gresource.xml | 1 +
data/resources/application.css | 5 +
data/resources/main-window.ui | 520 +++++++++++-------
data/resources/result-list-row.ui | 72 +++
src/application.js | 11 +-
src/logManager.js | 1 +
src/mainWindow.js | 64 +++-
src/org.gnome.Polari.src.gresource.xml | 3 +
src/resultList.js | 379 +++++++++++++
src/resultStack.js | 48 ++
src/resultView.js | 872 ++++++++++++++++++++++++++++++
src/utils.js | 96 ++++
12 files changed, 1863 insertions(+), 209 deletions(-)
---
diff --git a/data/org.gnome.Polari.data.gresource.xml b/data/org.gnome.Polari.data.gresource.xml
index 542288e..bc87cfb 100644
--- a/data/org.gnome.Polari.data.gresource.xml
+++ b/data/org.gnome.Polari.data.gresource.xml
@@ -10,6 +10,7 @@
<file alias="ui/entry-area.ui" preprocess="xml-stripblanks">resources/entry-area.ui</file>
<file alias="ui/join-room-dialog.ui" preprocess="xml-stripblanks">resources/join-room-dialog.ui</file>
<file alias="ui/main-window.ui" preprocess="xml-stripblanks">resources/main-window.ui</file>
+ <file alias="ui/result-list-row.ui" preprocess="xml-stripblanks">resources/result-list-row.ui</file>
<file alias="ui/room-list-header.ui" preprocess="xml-stripblanks">resources/room-list-header.ui</file>
<file alias="ui/room-list-row.ui" preprocess="xml-stripblanks">resources/room-list-row.ui</file>
<file alias="ui/user-list-details.ui" preprocess="xml-stripblanks">resources/user-list-details.ui</file>
diff --git a/data/resources/application.css b/data/resources/application.css
index 17b0a9c..20c775d 100644
--- a/data/resources/application.css
+++ b/data/resources/application.css
@@ -66,6 +66,11 @@
color: mix(@theme_fg_color, @theme_base_color, 0.3);
}
+.content-label {
+ color: mix(@theme_fg_color, @theme_base_color, 0.3);
+ font-size: small;
+}
+
.polari-room-list row.inactive:selected *,
.polari-room-list row.inactive:selected:backdrop * {
color: mix(@theme_selected_fg_color, @theme_base_color, 0.3);
diff --git a/data/resources/main-window.ui b/data/resources/main-window.ui
index 78a38b5..c498cd1 100644
--- a/data/resources/main-window.ui
+++ b/data/resources/main-window.ui
@@ -1,224 +1,330 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
- <object class="Gjs_UserListPopover" id="userListPopover">
- <property name="position">bottom</property>
- <property name="border-width">6</property>
- <property name="width-request">250</property>
- <property name="relative-to">showUserListButton</property>
- <style>
- <class name="polari-user-list"/>
- </style>
- </object>
- <template class="Gjs_MainWindow">
- <property name="title" translatable="yes">Polari</property>
- <property name="icon-name">org.gnome.Polari</property>
- <child type="titlebar">
- <object class="GtkBox">
- <property name="visible">True</property>
- <child>
- <object class="GtkHeaderBar" id="titlebarLeft">
- <property name="visible">True</property>
- <property name="hexpand">False</property>
- <property name="show-close-button">True</property>
- <child>
- <object class="GtkButton" id="joinButton">
- <property name="visible">True</property>
- <property name="halign">end</property>
- <property name="valign">center</property>
- <property name="margin-start">5</property>
- <property name="margin-end">5</property>
- <property name="action_name">app.show-join-dialog</property>
- <style>
- <class name="image-button"/>
- </style>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="icon-name">list-add-symbolic</property>
- <property name="icon-size">1</property>
- </object>
- </child>
- <child internal-child="accessible">
- <object class="AtkObject">
- <property name="AtkObject::accessible-name"
- translatable="yes">Add rooms and networks</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="pack-type">start</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <style>
- <class name="polari-titlebar-separator"/>
- </style>
- </object>
- <packing>
- <property name="pack-type">end</property>
- </packing>
- </child>
- </object>
- </child>
- <child>
- <object class="GtkHeaderBar" id="titlebarRight">
- <property name="visible">True</property>
- <property name="hexpand">True</property>
- <property name="show-close-button">True</property>
- <!-- Use a custom title widget to enable markup for subtitles
- (for URLs in channel topics); other than that, we want
- the default GtkHeaderBar behavior, e.g. the subtitle may
- be hidden, but is always included in the size request.
- We replicate this by using a stack which will only ever show
- its first child, but still consider the second one's size -->
- <child type="title">
- <object class="GtkStack">
+ <object class="Gjs_UserListPopover" id="userListPopover">
+ <property name="position">bottom</property>
+ <property name="border-width">6</property>
+ <property name="width-request">250</property>
+ <property name="relative-to">showUserListButton</property>
+ <style>
+ <class name="polari-user-list"/>
+ </style>
+ </object>
+ <template class="Gjs_MainWindow">
+ <property name="title" translatable="yes">Polari</property>
+ <property name="icon-name">org.gnome.Polari</property>
+ <child type="titlebar">
+ <object class="GtkBox">
<property name="visible">True</property>
- <property name="margin-start">24</property>
- <property name="margin-end">24</property>
<child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <property name="valign">center</property>
- <child>
- <object class="GtkLabel">
+ <object class="GtkHeaderBar" id="titlebarLeft">
<property name="visible">True</property>
- <property name="single-line-mode">True</property>
- <property name="ellipsize">end</property>
- <property name="label" bind-source="Gjs_MainWindow"
- bind-property="title" bind-flags="sync-create"/>
- <style>
- <class name="title"/>
- </style>
- </object>
- </child>
- <child>
- <object class="GtkLabel">
- <property name="visible" bind-source="Gjs_MainWindow"
- bind-property="subtitle-visible"
- bind-flags="sync-create"/>
- <property name="single-line-mode">True</property>
- <property name="ellipsize">end</property>
- <property name="use-markup">True</property>
- <property name="label" bind-source="Gjs_MainWindow"
- bind-property="subtitle" bind-flags="sync-create"/>
- <style>
- <class name="subtitle"/>
- <class name="dim-label"/>
- </style>
- </object>
- </child>
- </object>
+ <property name="hexpand">False</property>
+ <property name="show-close-button">True</property>
+ <child>
+ <object class="GtkButton" id="joinButton">
+ <property name="visible">True</property>
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <property name="margin-start">5</property>
+ <property name="margin-end">5</property>
+ <property name="action_name">app.show-join-dialog</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">list-add-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ <child internal-child="accessible">
+ <object class="AtkObject">
+ <property name="AtkObject::accessible-name"
+ translatable="yes">Add rooms and networks</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack-type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <style>
+ <class name="polari-titlebar-separator"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack-type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="searchButton">
+ <property name="visible">True</property>
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <property name="margin-start">5</property>
+ <property name="margin-end">5</property>
+ <property name="action-name">app.toggle-search</property>
+ <property name="active" bind-source="searchBar"
+ bind-property="search-mode-enabled"
bind-flags="sync-create|bidirectional"/>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">edit-find-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ <child internal-child="accessible">
+ <object class="AtkObject">
+ <property name="AtkObject::accessible-name"
+ translatable="yes">Add rooms and networks</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack-type">end</property>
+ </packing>
+ </child>
+ </object>
</child>
<child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <object class="GtkLabel">
+ <object class="GtkHeaderBar" id="titlebarRight">
<property name="visible">True</property>
- <property name="single-line-mode">True</property>
- <property name="ellipsize">end</property>
- <style>
- <class name="title"/>
- </style>
- </object>
- </child>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="single-line-mode">True</property>
- <property name="ellipsize">end</property>
- <property name="use-markup">True</property>
- <style>
- <class name="subtitle"/>
- <class name="dim-label"/>
- </style>
- </object>
- </child>
- </object>
+ <property name="hexpand">True</property>
+ <property name="show-close-button">True</property>
+ <!-- Use a custom title widget to enable markup for subtitles
+ (for URLs in channel topics); other than that, we want
+ the default GtkHeaderBar behavior, e.g. the subtitle may
+ be hidden, but is always included in the size request.
+ We replicate this by using a stack which will only ever show
+ its first child, but still consider the second one's size -->
+ <child type="title">
+ <object class="GtkStack">
+ <property name="visible">True</property>
+ <property name="margin-start">24</property>
+ <property name="margin-end">24</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="valign">center</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="single-line-mode">True</property>
+ <property name="ellipsize">end</property>
+ <property name="label" bind-source="Gjs_MainWindow"
+ bind-property="title" bind-flags="sync-create"/>
+ <style>
+ <class name="title"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible" bind-source="Gjs_MainWindow"
+ bind-property="subtitle-visible"
+ bind-flags="sync-create"/>
+ <property name="single-line-mode">True</property>
+ <property name="ellipsize">end</property>
+ <property name="use-markup">True</property>
+ <property name="label" bind-source="Gjs_MainWindow"
+ bind-property="subtitle" bind-flags="sync-create"/>
+ <style>
+ <class name="subtitle"/>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="single-line-mode">True</property>
+ <property name="ellipsize">end</property>
+ <style>
+ <class name="title"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="single-line-mode">True</property>
+ <property name="ellipsize">end</property>
+ <property name="use-markup">True</property>
+ <style>
+ <class name="subtitle"/>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="showUserListButton">
+ <property name="visible">True</property>
+ <property name="focus-on-click">False</property>
+ <property name="action-name">app.user-list</property>
+ <style>
+ <class name="polari-user-list-button"/>
+ <class name="text-button"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack-type">end</property>
+ </packing>
+ </child>
+ </object>
</child>
- </object>
- </child>
- <child>
- <object class="GtkToggleButton" id="showUserListButton">
- <property name="visible">True</property>
- <property name="focus-on-click">False</property>
- <property name="action-name">app.user-list</property>
- <style>
- <class name="polari-user-list-button"/>
- <class name="text-button"/>
- </style>
- </object>
- <packing>
- <property name="pack-type">end</property>
- </packing>
- </child>
- </object>
+ </object>
</child>
- </object>
- </child>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
<child>
- <object class="GtkRevealer" id="roomListRevealer">
- <property name="visible">True</property>
- <property name="hexpand">False</property>
- <property name="transition-type">slide-right</property>
- <child>
- <object class="Gjs_FixedSizeFrame" id="roomSidebar">
+ <object class="GtkBox">
<property name="visible">True</property>
- <property name="hexpand">False</property>
- <property name="width">200</property>
- <property name="shadow-type">none</property>
- <style>
- <class name="polari-room-list"/>
- </style>
<child>
- <object class="GtkScrolledWindow">
- <property name="visible">True</property>
- <property name="hscrollbar-policy">never</property>
- <property name="vexpand">True</property>
- <property name="hexpand">True</property>
- <child>
- <object class="Gjs_RoomList">
+ <object class="GtkRevealer" id="roomListRevealer">
<property name="visible">True</property>
- <property name="selection-mode">browse</property>
- <style>
- <class name="sidebar"/>
- </style>
- </object>
- </child>
- </object>
+ <property name="hexpand">False</property>
+ <property name="transition-type">slide-right</property>
+ <child>
+ <object class="Gjs_FixedSizeFrame" id="roomSidebar">
+ <property name="visible">True</property>
+ <property name="hexpand">False</property>
+ <property name="width">200</property>
+ <property name="shadow-type">none</property>
+ <style>
+ <class name="polari-room-list"/>
+ </style>
+ <child>
+ <object class="GtkGrid" id="sidebar-grid">
+ <property name="can_focus">False</property>
+ <property name="visible">True</property>
+ <property name="hexpand">False</property>
+ <property name="vexpand">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkSearchBar" id="searchBar">
+ <property name="visible">True</property>
+ <property name="halign">fill</property>
+ <child>
+ <object class="GtkSearchEntry" id="searchEntry">
+ <property name="can_focus">True</property>
+ <property name="halign">fill</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="leftStack">
+ <property name="visible">True</property>
+ <property name="transition-type">crossfade</property>
+ <property name="hexpand">true</property>
+ <!-- <property name="resize-mode">queue</property> -->
+ <property name="visible-child-name"
bind-source="Gjs_MainWindow"
+ bind-property="mode" bind-flags="sync-create"/>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="hscrollbar-policy">never</property>
+ <property name="vexpand">True</property>
+ <property name="hexpand">True</property>
+ <child>
+ <object class="Gjs_RoomList">
+ <property name="visible">True</property>
+ <property
name="selection-mode">browse</property>
+ <style>
+ <class name="sidebar"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">chat</property>
+ </packing>
+ </child>
+ <child>
+ <object class="Gjs_ResultWindow">
+ <property name="visible">True</property>
+ <property name="hscrollbar-policy">never</property>
+ <property name="vexpand">True</property>
+ <property name="hexpand">True</property>
+ <!-- <child>
+ <object class="Gjs_ResultList">
+ <property name="visible">True</property>
+ <property
name="selection-mode">browse</property>
+ <style>
+ <class name="sidebar"/>
+ </style>
+ </object>
+
+ </child> -->
+ </object>
+ <packing>
+ <property name="name">search</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
</child>
- </object>
- </child>
- </object>
- </child>
- <child>
- <object class="GtkOverlay" id="overlay">
- <property name="visible">True</property>
- <property name="vexpand">True</property>
- <child>
- <object class="Gjs_RoomStack" id="roomStack">
- <property name="visible">True</property>
- <property name="homogeneous">True</property>
- <property name="transition-type">crossfade</property>
- </object>
- </child>
- </object>
+ <child>
+ <object class="GtkOverlay" id="overlay">
+ <property name="visible">True</property>
+ <property name="vexpand">True</property>
+ <child>
+ <object class="GtkStack" id="rightStack">
+ <property name="visible">True</property>
+ <property name="transition-type">crossfade</property>
+ <property name="hexpand">true</property>
+ <property name="visible-child-name" bind-source="Gjs_MainWindow"
+ bind-property="mode" bind-flags="sync-create"/>
+ <child>
+ <object class="Gjs_RoomStack" id="roomStack">
+ <property name="visible">True</property>
+ <property name="homogeneous">True</property>
+ <property name="transition-type">crossfade</property>
+ </object>
+ <packing>
+ <property name="name">chat</property>
+ </packing>
+ </child>
+ <child>
+ <object class="Gjs_ResultStack" id="resultStack">
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">search</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
</child>
- </object>
- </child>
- </template>
- <object class="GtkSizeGroup">
- <widgets>
- <widget name="titlebarLeft"/>
- <widget name="roomSidebar"/>
- </widgets>
- </object>
+ </template>
+ <object class="GtkSizeGroup">
+ <widgets>
+ <widget name="titlebarLeft"/>
+ <widget name="roomSidebar"/>
+ </widgets>
+ </object>
</interface>
diff --git a/data/resources/result-list-row.ui b/data/resources/result-list-row.ui
new file mode 100644
index 0000000..eee0706
--- /dev/null
+++ b/data/resources/result-list-row.ui
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="Gjs_ResultRow" parent="GtkListBoxRow">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid" id="grid1">
+ <property name="visible">1</property>
+ <property name="hexpand">1</property>
+ <child>
+ <object class="GtkBox" id="box1">
+ <property name="visible">1</property>
+ <property name="hexpand">1</property>
+ <property name="baseline_position">top</property>
+ <child>
+ <object class="GtkLabel" id="source_name">
+ <property name="visible">1</property>
+ <property name="valign">baseline</property>
+ <property name="label" translatable="0">Username</property>
+ <property name="ellipsize">end</property>
+ <attributes>
+ <!-- <attribute name="weight" value="bold"/> -->
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="short_time_label">
+ <property name="visible">1</property>
+ <property name="valign">baseline</property>
+ <property name="label" translatable="yes">38m</property>
+ <property name="ellipsize">end</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="fill">0</property>
+ <property name="pack_type">end</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="content_label">
+ <property name="visible">1</property>
+ <property name="halign">start</property>
+ <property name="valign">start</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="0">Message</property>
+ <property name="wrap">1</property>
+ <property name="ellipsize">end</property>
+ <property name="max-width-chars">30</property>
+ <property name="use-markup">true</property>
+ <property name="lines">2</property>
+ <style>
+ <class name="content-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/application.js b/src/application.js
index a7dbd0d..2608c00 100644
--- a/src/application.js
+++ b/src/application.js
@@ -113,7 +113,16 @@ const Application = new Lang.Class({
{ name: 'next-pending-room',
accels: ['<Alt><Shift>Down', '<Primary><Shift>Page_Down']},
{ name: 'previous-pending-room',
- accels: ['<Alt><Shift>Up', '<Primary><Shift>Page_Up']}
+ accels: ['<Alt><Shift>Up', '<Primary><Shift>Page_Up']},
+ { name: 'toggle-search',
+ activate: Lang.bind(this, this._onToggleAction),
+ state: GLib.Variant.new('b', false),
+ accels: ['<Primary>f','<Primary>s'] },
+ { name: 'search-terms',
+ parameter_type: GLib.VariantType.new('s'),
+ state: GLib.Variant.new('s', '') },
+ { name: 'active-result-changed',
+ parameter_type: GLib.VariantType.new('(suss)') }
];
actionEntries.forEach(Lang.bind(this,
function(actionEntry) {
diff --git a/src/logManager.js b/src/logManager.js
index 0f0c80d..6c0c48a 100644
--- a/src/logManager.js
+++ b/src/logManager.js
@@ -103,6 +103,7 @@ const GenericQuery = new Lang.Class({
_getColumnsValue: function(cursor, col) {
switch(cursor.get_value_type(col)) {
case Tracker.SparqlValueType.STRING:
+ case Tracker.SparqlValueType.URI:
return cursor.get_string(col)[0];
case Tracker.SparqlValueType.INTEGER:
return cursor.get_integer(col);
diff --git a/src/mainWindow.js b/src/mainWindow.js
index a46b113..c11d8d7 100644
--- a/src/mainWindow.js
+++ b/src/mainWindow.js
@@ -8,16 +8,24 @@ const Tp = imports.gi.TelepathyGLib;
const AccountsMonitor = imports.accountsMonitor;
const AppNotifications = imports.appNotifications;
const ChatroomManager = imports.chatroomManager;
+const LogManager = imports.logManager;
const JoinDialog = imports.joinDialog;
const Lang = imports.lang;
const Mainloop = imports.mainloop;
const RoomList = imports.roomList;
const RoomStack = imports.roomStack;
+//const ResultStack = imports.resultStack;
const UserList = imports.userList;
const Utils = imports.utils;
+const Pango = imports.gi.Pango;
+const ChatView = imports.chatView;
+const ResultList = imports.resultList;
+const ResultView = imports.resultView;
+const ResultStack = imports.resultStack;
const CONFIGURE_TIMEOUT = 100; /* ms */
+const MIN_SEARCH_WIDTH = 0;
const FixedSizeFrame = new Lang.Class({
Name: 'FixedSizeFrame',
@@ -94,6 +102,8 @@ const MainWindow = new Lang.Class({
InternalChildren: ['titlebarRight',
'titlebarLeft',
'joinButton',
+ 'searchBar',
+ 'searchEntry',
'showUserListButton',
'userListPopover',
'roomListRevealer',
@@ -109,11 +119,17 @@ const MainWindow = new Lang.Class({
'subtitle-visible',
'subtitle-visible',
GObject.ParamFlags.READABLE,
- false)
+ false),
+ 'mode' : GObject.ParamSpec.string('mode',
+ 'mode',
+ 'mode',
+ GObject.ParamFlags.READABLE,
+ 'chat')
},
_init: function(params) {
this._subtitle = '';
+ this._mode = 'chat';
params.show_menubar = false;
this.parent(params);
@@ -195,6 +211,19 @@ const MainWindow = new Lang.Class({
Lang.bind(this, this._onSizeAllocate));
this.connect('delete-event',
Lang.bind(this, this._onDelete));
+ // this.connect('key-press-event', Lang.bind(this, this._handleKeyPress));
+
+ // search start
+ this._keywords = [];
+
+ this._searchBar.connect_entry(this._searchEntry);
+ this._searchBar.connect('notify::search-mode-enabled',
+ Lang.bind(this, this._updateMode));
+ this._searchEntry.connect('search-changed',
+ Lang.bind(this, this._handleSearchChanged));
+
+ this._logManager = LogManager.getDefault();
+ // search end
let size = this._settings.get_value('window-size').deep_unpack();
if (size.length == 2)
@@ -214,6 +243,35 @@ const MainWindow = new Lang.Class({
return this._subtitle.length > 0;
},
+ get mode() {
+ return this._mode;
+ },
+
+ _updateMode: function() {
+ let mode;
+ if (this._mode == 'search') {
+ mode = this._searchBar.search_mode_enabled ? 'search' : 'chat';
+ } else {
+ let state = this.application.get_action_state('search-terms');
+ let [terms, ] = state.get_string();
+ mode = terms.length > 0 ? 'search' : 'chat';
+ }
+
+ if (mode == this._mode)
+ return;
+
+ this._mode = mode;
+ this.notify('mode');
+ },
+
+ _handleSearchChanged: function(entry) {
+ let text = entry.get_text().replace(/^\s+|\s+$/g, '');
+ let terms = new GLib.Variant('s',
+ text.length < MIN_SEARCH_WIDTH ? '' : text);
+ this.application.change_action_state('search-terms', terms);
+ this._updateMode();
+ },
+
_onWindowStateEvent: function(widget, event) {
let state = event.get_window().get_state();
@@ -221,6 +279,10 @@ const MainWindow = new Lang.Class({
this._isMaximized = (state & Gdk.WindowState.MAXIMIZED) != 0;
},
+ _handleKeyPress: function(self, event) {
+ return this._searchBar.handle_event(event);
+ },
+
_onSizeAllocate: function(widget, allocation) {
if (!this._isFullscreen && !this._isMaximized)
this._currentSize = this.get_size(this);
diff --git a/src/org.gnome.Polari.src.gresource.xml b/src/org.gnome.Polari.src.gresource.xml
index a0b7541..441772b 100644
--- a/src/org.gnome.Polari.src.gresource.xml
+++ b/src/org.gnome.Polari.src.gresource.xml
@@ -15,8 +15,11 @@
<file>mainWindow.js</file>
<file>networksManager.js</file>
<file>pasteManager.js</file>
+ <file>resultList.js</file>
+ <file>resultView.js</file>
<file>roomList.js</file>
<file>roomStack.js</file>
+ <file>resultStack.js</file>
<file>tabCompletion.js</file>
<file>userList.js</file>
<file>utils.js</file>
diff --git a/src/resultList.js b/src/resultList.js
new file mode 100644
index 0000000..9f32786
--- /dev/null
+++ b/src/resultList.js
@@ -0,0 +1,379 @@
+const Gdk = imports.gi.Gdk;
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Gtk = imports.gi.Gtk;
+const Pango = imports.gi.Pango;
+const Tp = imports.gi.TelepathyGLib;
+
+const LogManager = imports.logManager;
+const AccountsMonitor = imports.accountsMonitor;
+const ChatroomManager = imports.chatroomManager;
+const Lang = imports.lang;
+const Signals = imports.signals;
+const Mainloop = imports.mainloop;
+const Utils = imports.utils;
+
+const MIN_SEARCH_WIDTH = 0;
+
+const ResultRow = new Lang.Class({
+ Name: 'ResultRow',
+ Extends: Gtk.ListBoxRow,
+ Template: 'resource:///org/gnome/Polari/ui/result-list-row.ui',
+ InternalChildren: ['box1', 'source_name', 'short_time_label', 'content_label'],
+
+ _init: function(event) {
+ this.parent();
+ this._source_name.label = event.chan.substring(1);
+ this._short_time_label.label = Utils.formatTimestamp(event.timestamp);
+ this.uid = event.id;
+ this.channel = event.chan;
+ this.nickname = event.chan;
+ this.timestamp = event.timestamp;
+ this.rawmessage = event.mms;
+
+ // this._icon.gicon = room.icon;
+ // this._icon.visible = room.icon != null;
+ this.connect('key-press-event',
+ Lang.bind(this, this._onKeyPress));
+
+ // room.connect('notify::channel', Lang.bind(this,
+ // function() {
+ // if (!room.channel)
+ // return;
+ // room.channel.connect('message-received',
+ // Lang.bind(this, this._updatePending));
+ // room.channel.connect('pending-message-removed',
+ // Lang.bind(this, this._updatePending));
+ // }));
+ // room.bind_property('display-name', this._roomLabel, 'label',
+ // GObject.BindingFlags.SYNC_CREATE);
+ //
+ // this._updatePending();
+ },
+
+ _onButtonRelease: function(w, event) {
+ let [, button] = event.get_button();
+ if (button != Gdk.BUTTON_SECONDARY)
+ return Gdk.EVENT_PROPAGATE;
+
+ // this._showPopover();
+
+ return Gdk.EVENT_STOP;
+ },
+
+ _onKeyPress: function(w, event) {
+ let [, keyval] = event.get_keyval();
+ let [, mods] = event.get_state();
+ if (keyval != Gdk.KEY_Menu &&
+ !(keyval == Gdk.KEY_F10 &&
+ mods & Gdk.ModifierType.SHIFT_MASK))
+ return Gdk.EVENT_PROPAGATE;
+
+ // this._showPopover();
+
+ return Gdk.EVENT_STOP;
+ }
+});
+
+const ResultList = new Lang.Class({
+ Name: 'ResultList',
+ Extends: Gtk.ListBox,
+
+ _init: function(params) {
+ this.parent(params);
+ this._app = Gio.Application.get_default();
+ this._logManager = LogManager.getDefault();
+
+ //this.connect('row-activated', Lang.bind(this, this._rowactivated));
+ this._results = [];
+ this._widgetMap = {};
+ //this._keywordsAction = app.lookup_action('search-terms');
+ this._app.connect('action-state-changed::search-terms', Lang.bind(this,
+ this._handleSearchChanged));
+
+ this.connect('scroll-bottom-reached', Lang.bind(this, this._loadNextResults));
+
+ this._fetchingResults = false;
+ this._keywords = [];
+ this._keywordsText = '';
+ this._cancellable = new Gio.Cancellable();
+ },
+
+ vfunc_row_selected: function(row) {
+ if(!row) return;
+ let rowSelectedAction = this._app.lookup_action('active-result-changed');
+ rowSelectedAction.activate(new GLib.Variant('(suss)', [row.uid, row.timestamp, row.channel,
this._keywordsText]));
+ // if (row)
+ // row.selected();
+ },
+
+ _clearList: function() {
+ this.foreach(r => { r.hide(); });
+ },
+
+ _showList: function() {
+ this.foreach(r => { r.show(); });
+ },
+
+ _handleSearchChanged: function(group, actionName, value) {
+ this._cancellable.cancel();
+ // this._cancellable.reset();
+ this._cancellable = new Gio.Cancellable();
+ let text = value.deep_unpack();
+ this._clearList();
+ this._results = [];
+ this.set_placeholder(null);
+
+ if(text.length < MIN_SEARCH_WIDTH) {
+ return;
+ }
+
+ this._keywordsText = text;
+ this._keywords = text == '' ? [] : text.split(/\s+/);
+ log(text);
+ let query = ('select ?text as ?mms ?msg as ?id ?chan as ?chan ?timestamp as ?timestamp ' +
+ 'where { ?msg a nmo:IMMessage . ?msg nie:plainTextContent ?text . ?msg fts:match "%s*"
. ' +
+ '?msg nmo:communicationChannel ?channel. ?channel nie:title ?chan. ' +
+ '?msg nie:contentCreated ?timestamp } order by desc (?timestamp)'
+ ).format(text);
+ log(query);
+ this._fetchingResults = true;
+ this._endQuery = new LogManager.GenericQuery(this._logManager._connection, 20);
+ this._endQuery.run(query,this._cancellable,Lang.bind(this, this._handleResults));
+ Mainloop.timeout_add(3000, Lang.bind(this,
+ function() {
+ if(this._fetchingResults) {
+ let placeholder = new Gtk.Box({ halign: Gtk.Align.CENTER,
+ valign: Gtk.Align.CENTER,
+ orientation: Gtk.Orientation.HORIZONTAL,
+ visible: true });
+ let spinner = new Gtk.Spinner({ visible: true });
+ spinner.start();
+ placeholder.add(spinner);
+ placeholder.add(new Gtk.Label({ label: _(" Searching.."),
+ visible: true }));
+
+ placeholder.get_style_context().add_class('dim-label');
+ this.set_placeholder(placeholder);
+ }
+ // else this.set_placeholder(null);
+ return GLib.SOURCE_REMOVE;
+ }));
+ // this._logManager.query(query,this._cancellable,Lang.bind(this, this._handleResults));
+ },
+
+ _loadNextResults: function() {
+ print("here");
+ if (this._fetchingResults)
+ return;
+ print("and here");
+ this._fetchingResults = true;
+
+ Mainloop.timeout_add(500, Lang.bind(this,
+ function() {
+ this._endQuery.next(10,this._cancellable,Lang.bind(this, this._handleResults1));
+ }));
+
+ },
+
+ _handleResults: function(events) {
+ log(events.length);
+ if(events.length == 0) {
+ let placeholder = new Gtk.Box({ halign: Gtk.Align.CENTER,
+ valign: Gtk.Align.CENTER,
+ orientation: Gtk.Orientation.VERTICAL,
+ visible: true });
+ placeholder.add(new Gtk.Image({ icon_name: 'edit-find-symbolic',
+ pixel_size: 64,
+ visible: true }));
+ placeholder.add(new Gtk.Label({ label: _("No results"),
+ visible: true }));
+
+ placeholder.get_style_context().add_class('dim-label');
+ this.set_placeholder(placeholder);
+ }
+ let widgetMap = {};
+ let markup_message = '';
+ for (let i = 0; i < events.length; i++) {
+ let message = GLib.markup_escape_text(events[i].mms, -1);
+ let uid = events[i].id;
+ let row;
+ row = this._widgetMap[uid];
+ for (let j = 0; j < this._keywords.length; j++) {
+ // log(this._keywords[j]);
+ // index = Math.min(index, message.indexOf(this._keywords[j]));
+ // message = message.replace( new RegExp( "(" + this._keywords[j] + ")" , 'gi' ),"<span
font_weight='bold'>$1</span>");
+ // print(message);
+ }
+
+ if (row) {
+ widgetMap[uid] = row;
+ this.remove(row);
+ } else {
+ row = new ResultRow(events[i]);
+ widgetMap[uid] = row;
+ }
+ row._content_label.label = message;
+ }
+
+ this._widgetMap = widgetMap;
+
+ this.foreach(r => { r.destroy(); })
+
+ for (let i = 0; i < events.length; i++) {
+ let row = this._widgetMap[events[i].id];
+ this.add(row);
+ }
+
+ if(events.length > 0) {
+ let row = this._widgetMap[events[0].id];
+ this.select_row(row);
+ }
+
+ this._showList();
+ this._fetchingResults = false;
+ },
+
+ _handleResults1: function(events){
+ log(events.length);
+ for (let i = 0; i < events.length; i++) {
+ let message = GLib.markup_escape_text(events[i].mms, -1);
+ let uid = events[i].id;
+ let row;
+ row = new ResultRow(events[i]);
+ this._widgetMap[uid] = row;
+ row._content_label.label = message;
+ this.add(row);
+ }
+
+ this._showList();
+ this._fetchingResults = false;
+ }
+});
+
+const ResultWindow = new Lang.Class({
+ Name: 'ResultWindow',
+ Extends: Gtk.ScrolledWindow,
+
+ _init: function(params) {
+ this.parent(params);
+
+ this._list = new ResultList({ visible: true, selection_mode: Gtk.SelectionMode.BROWSE });
+ // this._list.set_placeholder(new ResultPlaceholder());
+ this.add(this._list);
+ this.show_all();
+
+ this._cancellable = new Gio.Cancellable();
+
+ this.connect('scroll-event', Lang.bind(this, this._onScroll));
+
+ this.vadjustment.connect('changed',
+ Lang.bind(this, this._updateScroll));
+
+ let adj = this.vadjustment;
+ this._scrollBottom = adj.upper - adj.page_size;
+ print(this._scrollBottom);
+ this._hoverCursor = Gdk.Cursor.new(Gdk.CursorType.HAND1);
+ },
+
+ _updateScroll: function() {
+ let adj = this.vadjustment;
+ this._scrollBottom = adj.upper - adj.page_size;
+ },
+
+ _onScroll: function(w, event) {
+ let [hasDir, dir] = event.get_scroll_direction();
+ if (hasDir && (dir != Gdk.ScrollDirection.UP || dir != Gdk.ScrollDirection.DOWN) )
+ return Gdk.EVENT_PROPAGATE;
+
+ let [hasDeltas, dx, dy] = event.get_scroll_deltas();
+ if (hasDeltas)
+ this._fetchMoreResults();
+ },
+
+ _fetchMoreResults: function() {
+ if (this.vadjustment.value != this._scrollBottom )
+ return Gdk.EVENT_PROPAGATE;
+
+ this._list.emit('scroll-bottom-reached');
+
+ return Gdk.EVENT_STOP;
+ },
+});
+
+const ResultPlaceholder = new Lang.Class({
+ Name: 'ResultPlaceholder',
+ Extends: Gtk.Overlay,
+
+ _init: function() {
+ let image = new Gtk.Image({ icon_name: 'org.gnome.Polari-symbolic',
+ pixel_size: 96, halign: Gtk.Align.END,
+ margin_end: 14 });
+
+ let title = new Gtk.Label({ use_markup: true, halign: Gtk.Align.START,
+ margin_start: 14 });
+ title.label = '<span letter_spacing="4500">%s</span>'.format(_("Polari"));
+ title.get_style_context().add_class('polari-background-title');
+
+ let description = new Gtk.Label({ label: _("Join a room using the + button."),
+ halign: Gtk.Align.CENTER, wrap: true,
+ margin_top: 24, use_markup: true });
+ description.get_style_context().add_class('polari-background-description');
+
+ let inputPlaceholder = new Gtk.Box({ valign: Gtk.Align.END });
+ //sizeGroup.add_widget(inputPlaceholder);
+
+ this.parent();
+ let grid = new Gtk.Grid({ column_homogeneous: true, can_focus: false,
+ column_spacing: 18, hexpand: true, vexpand: true,
+ valign: Gtk.Align.CENTER });
+ grid.get_style_context().add_class('polari-background');
+ grid.attach(image, 0, 0, 1, 1);
+ grid.attach(title, 1, 0, 1, 1);
+ grid.attach(description, 0, 1, 2, 1);
+ this.add(grid);
+ this.add_overlay(inputPlaceholder);
+ this.show_all();
+ }
+});
+
+const LoadPlaceholder = new Lang.Class({
+ Name: 'LoadPlaceholder',
+ Extends: Gtk.Overlay,
+
+ _init: function() {
+ let image = new Gtk.Image({ icon_name: 'org.gnome.Polari-symbolic',
+ pixel_size: 96, halign: Gtk.Align.END,
+ margin_end: 14 });
+
+ let title = new Gtk.Label({ use_markup: true, halign: Gtk.Align.START,
+ margin_start: 14 });
+ title.label = '<span letter_spacing="4500">%s</span>'.format(_("Loading"));
+ title.get_style_context().add_class('polari-background-title');
+
+ let description = new Gtk.Label({ label: _("Join a room using the + button."),
+ halign: Gtk.Align.CENTER, wrap: true,
+ margin_top: 24, use_markup: true });
+ description.get_style_context().add_class('polari-background-description');
+
+ let inputPlaceholder = new Gtk.Box({ valign: Gtk.Align.END });
+ //sizeGroup.add_widget(inputPlaceholder);
+
+ this.parent();
+ let grid = new Gtk.Grid({ column_homogeneous: true, can_focus: false,
+ column_spacing: 18, hexpand: true, vexpand: true,
+ valign: Gtk.Align.CENTER });
+ grid.get_style_context().add_class('polari-background');
+ let spinner = new Gtk.Spinner({visible: true, active: true});
+ spinner.start();
+ grid.attach(spinner, 0, 0, 1, 1);
+ grid.attach(title, 1, 0, 1, 1);
+ // grid.attach(description, 0, 1, 2, 1);
+ this.add(grid);
+ this.add_overlay(inputPlaceholder);
+ this.show_all();
+ }
+});
+
+Signals.addSignalMethods(ResultList.prototype);
diff --git a/src/resultStack.js b/src/resultStack.js
new file mode 100644
index 0000000..8727b33
--- /dev/null
+++ b/src/resultStack.js
@@ -0,0 +1,48 @@
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Gtk = imports.gi.Gtk;
+
+const AccountsMonitor = imports.accountsMonitor;
+const ChatroomManager = imports.chatroomManager;
+const ResultView = imports.resultView;
+const Lang = imports.lang;
+
+const ResultStack = new Lang.Class({
+ Name: 'ResultStack',
+ Extends: Gtk.Stack,
+
+ _init: function(params) {
+ this.parent(params);
+
+ this._results = {};
+
+ this._app = Gio.Application.get_default();
+ this._activeResultAction = this._app.lookup_action('active-result-changed');
+ this._activeResultAction.connect('activate',
+ Lang.bind(this, this._activeResultChanged));
+
+ },
+
+ _addView: function(id, view) {
+ this._results[id] = view;
+ this.add_named(view, id);
+ },
+
+ _resultAdded: function(uid, timestamp, channel, keywords) {
+ this._addView(uid, new ResultView.ResultView(uid, timestamp, channel, keywords));
+ },
+
+ _resultRemoved: function(row) {
+ this._results[row.uid].destroy();
+ delete this._results[row.uid];
+ },
+
+ _activeResultChanged: function(action, parameter) {
+ let [uid, timestamp, channel, keywords] = parameter.deep_unpack();
+ print(uid);
+ if(!this._results[uid])
+ this._resultAdded(uid, timestamp, channel, keywords);
+ this.set_visible_child_name(uid);
+ }
+});
diff --git a/src/resultView.js b/src/resultView.js
new file mode 100644
index 0000000..7bc01f3
--- /dev/null
+++ b/src/resultView.js
@@ -0,0 +1,872 @@
+const Gdk = imports.gi.Gdk;
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const GObject = imports.gi.GObject;
+const Gtk = imports.gi.Gtk;
+const Pango = imports.gi.Pango;
+const PangoCairo = imports.gi.PangoCairo;
+const Polari = imports.gi.Polari;
+const Tp = imports.gi.TelepathyGLib;
+const Tpl = imports.gi.TelepathyLogger;
+
+const Lang = imports.lang;
+const LogManager = imports.logManager;
+const Mainloop = imports.mainloop;
+const PasteManager = imports.pasteManager;
+const Signals = imports.signals;
+const Utils = imports.utils;
+
+const MAX_NICK_CHARS = 8;
+const IGNORE_STATUS_TIME = 5;
+
+const SCROLL_TIMEOUT = 100; // ms
+
+const TIMESTAMP_INTERVAL = 300; // seconds of inactivity after which to
+ // insert a timestamp
+
+const INACTIVITY_THRESHOLD = 300; // a threshold in seconds used to control
+ // the visibility of status messages
+const STATUS_NOISE_MAXIMUM = 4;
+
+const NUM_INITIAL_LOG_EVENTS = 50; // number of log events to fetch on start
+const NUM_LOG_EVENTS = 10; // number of log events to fetch when requesting more
+
+const MARGIN = 14;
+const NICK_SPACING = 14; // space after nicks, matching the following elements
+ // of the nick button in the entry area:
+ // 8px padding + 6px spacing
+
+const NICKTAG_PREFIX = 'nick';
+
+
+const ResultTextView = new Lang.Class({
+ Name: 'ResultTextView',
+ Extends: Gtk.TextView,
+
+ _init: function(params) {
+ this.parent(params);
+
+ this.buffer.connect('mark-set', Lang.bind(this, this._onMarkSet));
+ this.connect('screen-changed', Lang.bind(this, this._updateLayout));
+ },
+
+ vfunc_get_preferred_width: function() {
+ return [1, 1];
+ },
+
+ vfunc_style_updated: function() {
+ let context = this.get_style_context();
+ context.save();
+ context.add_class('dim-label');
+ context.set_state(Gtk.StateFlags.NORMAL);
+ this._dimColor = context.get_color(context.get_state());
+ context.restore();
+
+ this.parent();
+ },
+
+ vfunc_draw: function(cr) {
+ this.parent(cr);
+
+ let mark = this.buffer.get_mark('indicator-line');
+ if (!mark) {
+ cr.$dispose();
+ return Gdk.EVENT_PROPAGATE;
+ }
+
+ let iter = this.buffer.get_iter_at_mark(mark);
+ let location = this.get_iter_location(iter);
+ let [, y] = this.buffer_to_window_coords(Gtk.TextWindowType.TEXT,
+ location.x, location.y);
+
+ let tags = iter.get_tags();
+ let pixelsAbove = tags.reduce(function(prev, current) {
+ return Math.max(prev, current.pixels_above_lines);
+ }, this.get_pixels_above_lines());
+ let pixelsBelow = tags.reduce(function(prev, current) {
+ return Math.max(prev, current.pixels_below_lines);
+ }, this.get_pixels_below_lines());
+
+ let lineSpace = Math.floor((pixelsAbove + pixelsBelow) / 2);
+ y = y - lineSpace + 0.5;
+
+ let width = this.get_allocated_width() - 2 * MARGIN;
+
+ let [, extents] = this._layout.get_pixel_extents();
+ let layoutWidth = extents.width + 0.5;
+ let layoutX = extents.x + Math.floor((width - extents.width) / 2) + 0.5;
+ let layoutHeight = extents.height;
+ let baseline = Math.floor(this._layout.get_baseline() / Pango.SCALE);
+ let layoutY = y - baseline + Math.floor((layoutHeight - baseline) / 2) + 0.5;
+
+ let [hasClip, clip] = Gdk.cairo_get_clip_rectangle(cr);
+ if (hasClip &&
+ clip.y <= layoutY + layoutHeight &&
+ clip.y + clip.height >= layoutY) {
+
+ Gdk.cairo_set_source_rgba(cr, this._dimColor);
+
+ cr.moveTo(layoutX, layoutY);
+ PangoCairo.show_layout(cr, this._layout);
+
+ let [, color] = this.get_style_context().lookup_color('borders');
+ Gdk.cairo_set_source_rgba(cr, color);
+
+ cr.setLineWidth(1);
+ cr.moveTo(MARGIN, y);
+ cr.lineTo(layoutX - MARGIN, y);
+ cr.moveTo(layoutX + layoutWidth + MARGIN, y);
+ cr.lineTo(MARGIN + width, y);
+ cr.stroke();
+ }
+ cr.$dispose();
+
+ return Gdk.EVENT_PROPAGATE;
+ },
+
+ _onMarkSet: function(buffer, iter, mark) {
+ if (mark.name == 'indicator-line')
+ this.queue_draw();
+ },
+
+ _updateLayout: function() {
+ this._layout = this.create_pango_layout(null);
+ this._layout.set_markup('<small><b>%s</b></small>'.format(_("New Messages")), -1);
+ }
+});
+
+const ResultView = new Lang.Class({
+ Name: 'ResultView',
+ Extends: Gtk.ScrolledWindow,
+
+ _init: function(uid, timestamp, channel, keywordsText) {
+ //this.parent();
+ print("HELLO");
+ this.parent({ hscrollbar_policy: Gtk.PolicyType.NEVER, vexpand: true });
+
+ this._view = new ResultTextView({ editable: false, cursor_visible: false,
+ wrap_mode: Gtk.WrapMode.WORD_CHAR,
+ right_margin: MARGIN });
+ print(this._view);
+ this._view.add_events(Gdk.EventMask.LEAVE_NOTIFY_MASK |
+ Gdk.EventMask.ENTER_NOTIFY_MASK);
+ this.add(this._view);
+ this.show_all();
+
+ this._logManager = LogManager.getDefault();
+ this._cancellable = new Gio.Cancellable();
+
+ this._keywords = keywordsText == '' ? [] : keywordsText.split(/\s+/);
+ this._keyregExp = new RegExp( '(' + this._keywords.join('|')+ ')', 'gi');
+ print(this._keyregExp);
+ this._active = false;
+ this._toplevelFocus = false;
+ this._fetchingBacklog = false;
+ this._joinTime = 0;
+ this._maxNickChars = MAX_NICK_CHARS;
+ this._needsIndicator = true;
+ this._pending = {};
+ this._pendingLogs = [];
+ this._logWalker = null;
+
+ this._createTags();
+
+ this.connect('style-updated',
+ Lang.bind(this, this._onStyleUpdated));
+ this._onStyleUpdated();
+
+ this.connect('screen-changed',
+ Lang.bind(this, this._updateIndent));
+ this.connect('scroll-event', Lang.bind(this, this._onScroll));
+ // this.connect('edge-reached', Lang.bind(this, this._onEdgeReached));
+
+ this.vadjustment.connect('changed',
+ Lang.bind(this, this._updateScroll));
+
+ this._view.connect('key-press-event', Lang.bind(this, this._onKeyPress));
+ /* pick up DPI changes (e.g. via the 'text-scaling-factor' setting):
+ the default handler calls pango_cairo_context_set_resolution(), so
+ update the indent after that */
+ this._view.connect_after('style-updated',
+ Lang.bind(this, this._updateIndent));
+
+ let adj = this.vadjustment;
+ this._scrollBottom = adj.upper - adj.page_size;
+
+ this._hoverCursor = Gdk.Cursor.new(Gdk.CursorType.HAND1);
+ this._rowactivated(uid, channel, timestamp);
+ },
+
+ _rowactivated: function(uid, channel, timestamp) {
+ this._uid = uid;
+ this._cancellable.cancel();
+ this._cancellable.reset();
+ let sparql = (
+ 'select nie:plainTextContent(?msg) as ?message ' +
+ '?msg as ?id ' +
+ ' if (nmo:from(?msg) = nco:default-contact-me,' +
+ ' "%s", nco:nickname(nmo:from(?msg))) as ?sender ' +
+ // FIXME: how do we handle the "real" message type?
+ ' %d as ?messageType ' +
+ ' ?timestamp ' +
+ '{ ?msg a nmo:IMMessage; ' +
+ ' nie:contentCreated ?timestamp; ' +
+ ' nmo:communicationChannel ?chan . ' +
+ 'BIND( ?timestamp - %s as ?timediff ) . ' +
+ // FIXME: filter by account
+ ' filter (nie:title (?chan) = "%s" && ?timediff >= 0) ' +
+ '} order by asc (?timediff)'
+ ).format(channel,
+ Tp.ChannelTextMessageType.NORMAL,
+ timestamp,
+ channel);
+ // log(sparql);
+ let sparql1 = (
+ 'select nie:plainTextContent(?msg) as ?message ' +
+ '?msg as ?id ' +
+ ' if (nmo:from(?msg) = nco:default-contact-me,' +
+ ' "%s", nco:nickname(nmo:from(?msg))) as ?sender ' +
+ // FIXME: how do we handle the "real" message type?
+ ' %d as ?messageType ' +
+ ' ?timestamp ' +
+ '{ ?msg a nmo:IMMessage; ' +
+ ' nie:contentCreated ?timestamp; ' +
+ ' nmo:communicationChannel ?chan . ' +
+ 'BIND( %s - ?timestamp as ?timediff ) . ' +
+ // FIXME: filter by account
+ ' filter (nie:title (?chan) = "%s" && ?timediff > 0) ' +
+ '} order by asc (?timediff)'
+ ).format(channel,
+ Tp.ChannelTextMessageType.NORMAL,
+ timestamp,
+ channel);
+ // let logManager = LogManager.getDefault();
+ // this._logWalker = logManager.walkEvents(row,
+ // row.channel);
+ //
+ // this._fetchingBacklog = true;
+ // this._logWalker.getEvents(10,
+ // Lang.bind(this, this._onLogEventsReady));
+ // this._logManager.query(sparql,this._cancellable,Lang.bind(this, this._onLogEventsReady));
+ // this._logManager.query(sparql1,this._cancellable,Lang.bind(this, this._onLogEventsReady1));
+ let buffer = this._view.get_buffer();
+ let iter = buffer.get_end_iter();
+ buffer.set_text("",-1);
+ this._endQuery = new LogManager.GenericQuery(this._logManager._connection, 20);
+ this._endQuery.run(sparql,this._cancellable,Lang.bind(this, this._onLogEventsReady1));
+ // log("!");
+ this._startQuery = new LogManager.GenericQuery(this._logManager._connection, 20);
+ // Mainloop.timeout_add(500, Lang.bind(this,
+ // function() {
+ // query.run(sparql1,this._cancellable,Lang.bind(this, this._onLogEventsReady1));
+ // return GLib.SOURCE_REMOVE;
+ // }));
+ this._startQuery.run(sparql1,this._cancellable,Lang.bind(this, this._onLogEventsReady));
+ //print(this._endQuery.isClosed());
+
+ // Mainloop.timeout_add(5000, Lang.bind(this,
+ // function() {
+ // query.next(200,this._cancellable,Lang.bind(this, this._onLogEventsReady1));
+ // }));
+ // query.next(20,this._cancellable,Lang.bind(this, this._onLogEventsReady1));
+
+ //this._resultStack.buffer.insert(iter,row._content_label.label, -1);
+ // this._resultStack.label = row._content_label.label;
+ },
+
+ _createTags: function() {
+ let buffer = this._view.get_buffer();
+ let tagTable = buffer.get_tag_table();
+ let tags = [
+ { name: 'nick',
+ left_margin: MARGIN },
+ { name: 'gap',
+ pixels_above_lines: 10 },
+ { name: 'message',
+ indent: 0 },
+ { name: 'highlight',
+ weight: Pango.Weight.BOLD },
+ { name: 'status',
+ left_margin: MARGIN,
+ indent: 0,
+ justification: Gtk.Justification.RIGHT },
+ { name: 'timestamp',
+ left_margin: MARGIN,
+ indent: 0,
+ weight: Pango.Weight.BOLD,
+ justification: Gtk.Justification.RIGHT },
+ { name: 'action',
+ left_margin: MARGIN },
+ { name: 'url',
+ underline: Pango.Underline.SINGLE },
+ { name: 'indicator-line',
+ pixels_above_lines: 24 },
+ { name: 'loading',
+ justification: Gtk.Justification.CENTER }
+ ];
+ tags.forEach(function(tagProps) {
+ tagTable.add(new Gtk.TextTag(tagProps));
+ });
+ },
+
+ _onStyleUpdated: function() {
+ let context = this.get_style_context();
+ context.save();
+ context.add_class('dim-label');
+ context.set_state(Gtk.StateFlags.NORMAL);
+ let dimColor = context.get_color(context.get_state());
+ context.restore();
+
+ context.save();
+ context.set_state(Gtk.StateFlags.LINK);
+ let linkColor = context.get_color(context.get_state());
+ this._activeNickColor = context.get_color(context.get_state());
+
+ context.set_state(Gtk.StateFlags.LINK | Gtk.StateFlags.PRELIGHT);
+ this._hoveredLinkColor = context.get_color(context.get_state());
+ context.restore();
+
+ let desaturatedNickColor = (this._activeNickColor.red +
+ this._activeNickColor.blue +
+ this._activeNickColor.green) / 3;
+ this._inactiveNickColor = new Gdk.RGBA ({ red: desaturatedNickColor,
+ green: desaturatedNickColor,
+ blue: desaturatedNickColor,
+ alpha: 1.0 });
+ if (this._activeNickColor.equal(this._inactiveNickColor))
+ this._inactiveNickColor.alpha = 0.5;
+
+ context.save();
+ context.add_class('view');
+ context.set_state(Gtk.StateFlags.NORMAL);
+ this._statusHeaderHoverColor = context.get_color(context.get_state());
+ context.restore();
+
+ let buffer = this._view.get_buffer();
+ let tagTable = buffer.get_tag_table();
+ let tags = [
+ { name: 'status',
+ foreground_rgba: dimColor },
+ { name: 'timestamp',
+ foreground_rgba: dimColor },
+ { name: 'action',
+ foreground_rgba: dimColor },
+ { name: 'url',
+ foreground_rgba: linkColor }
+ ];
+ tags.forEach(function(tagProps) {
+ let tag = tagTable.lookup(tagProps.name);
+ for (let prop in tagProps) {
+ if (prop == 'name')
+ continue;
+ tag[prop] = tagProps[prop];
+ }
+ });
+
+ let offset = NICKTAG_PREFIX.length;
+ tagTable.foreach(Lang.bind(this, function(tag) {
+ if (tag._status)
+ this._setNickStatus(tag.name.substring(offset), tag._status);
+ }));
+ },
+
+ vfunc_destroy: function() {
+ this.parent();
+ },
+
+ _onLogEventsReady: function(events) {
+ // print(events);
+ events = events.reverse();
+ this._hideLoadingIndicator();
+
+ this._pendingLogs = events.concat(this._pendingLogs);
+ this._insertPendingLogs();
+ this._fetchingBacklog = false;
+ },
+
+ _onLogEventsReady1: function(events) {
+ // print(events);
+ //events = events.reverse();
+ this._hideLoadingIndicator1();
+
+ this._pendingLogs = events.concat(this._pendingLogs);
+ this._insertPendingLogs1();
+ let buffer = this._view.get_buffer();
+ let mark = buffer.get_mark('centre');
+ this._view.scroll_to_mark(mark, 0.0, true, 0, 0.5);
+ this._fetchingBacklog = false;
+ },
+
+ _insertPendingLogs: function() {
+ if (this._pendingLogs.length == 0)
+ return;
+
+ let index = -1;
+ let nick = this._pendingLogs[0].sender;
+ let type = this._pendingLogs[0].messageType;
+ /* if (!this._query.isClosed()) {
+ for (let i = 0; i < this._pendingLogs.length; i++)
+ if (this._pendingLogs[i].sender != nick ||
+ this._pendingLogs[i].messageType != type) {
+ index = i;
+ break;
+ }
+ } else {
+ index = 0;
+ }
+
+ if (index < 0)
+ return;*/
+ index = 0;
+ // print(this._pendingLogs);
+ let pending = this._pendingLogs.splice(index);
+ // print(this._pendingLogs);
+ print(pending);
+ let state = { lastNick: null, lastTimestamp: 0 };
+ let iter = this._view.buffer.get_start_iter();
+ for (let i = 0; i < pending.length; i++) {
+ let message = { nick: pending[i].sender,
+ text: pending[i].message,
+ timestamp: pending[i].timestamp,
+ messageType: pending[i].messageType,
+ shouldHighlight: false,
+ id: pending[i].id};
+ this._insertMessage(iter, message, state);
+ this._setNickStatus(message.nick, Tp.ConnectionPresenceType.OFFLINE);
+
+ if (!iter.is_end() || i < pending.length - 1)
+ this._view.buffer.insert(iter, '\n', -1);
+ }
+
+ if (!this._channel)
+ return;
+
+ if (this._room.type == Tp.HandleType.ROOM) {
+ let members = this._channel.group_dup_members_contacts();
+ for (let j = 0; j < members.length; j++)
+ this._setNickStatus(members[j].get_alias(),
+ Tp.ConnectionPresenceType.AVAILABLE);
+ } else {
+ this._setNickStatus(this._channel.connection.self_contact.get_alias(),
+ Tp.ConnectionPresenceType.AVAILABLE);
+ this._setNickStatus(this._channel.target_contact.get_alias(),
+ Tp.ConnectionPresenceType.AVAILABLE);
+ }
+ },
+
+ _insertPendingLogs1: function() {
+ if (this._pendingLogs.length == 0)
+ return;
+
+ let index = -1;
+ let nick = this._pendingLogs[0].sender;
+ let type = this._pendingLogs[0].messageType;
+ /* if (!this._query.isClosed()) {
+ for (let i = 0; i < this._pendingLogs.length; i++)
+ if (this._pendingLogs[i].sender != nick ||
+ this._pendingLogs[i].messageType != type) {
+ index = i;
+ break;
+ }
+ } else {
+ index = 0;
+ }
+
+ if (index < 0)
+ return;*/
+ index = 0;
+ // print(this._pendingLogs);
+ let pending = this._pendingLogs.splice(index);
+ // print(this._pendingLogs);
+ // print(pending);
+ let state = { lastNick: null, lastTimestamp: 0 };
+ let iter = this._view.buffer.get_end_iter();
+ for (let i = 0; i < pending.length; i++) {
+ let message = { nick: pending[i].sender,
+ text: pending[i].message,
+ timestamp: pending[i].timestamp,
+ messageType: pending[i].messageType,
+ shouldHighlight: false,
+ id: pending[i].id};
+ this._insertMessage(iter, message, state);
+ this._setNickStatus(message.nick, Tp.ConnectionPresenceType.OFFLINE);
+
+ //if (!iter.is_end() || i < pending.length - 1)
+ this._view.buffer.insert(iter, '\n', -1);
+ }
+
+ if (!this._channel)
+ return;
+
+ if (this._room.type == Tp.HandleType.ROOM) {
+ let members = this._channel.group_dup_members_contacts();
+ for (let j = 0; j < members.length; j++)
+ this._setNickStatus(members[j].get_alias(),
+ Tp.ConnectionPresenceType.AVAILABLE);
+ } else {
+ this._setNickStatus(this._channel.connection.self_contact.get_alias(),
+ Tp.ConnectionPresenceType.AVAILABLE);
+ this._setNickStatus(this._channel.target_contact.get_alias(),
+ Tp.ConnectionPresenceType.AVAILABLE);
+ }
+ },
+
+ get _nPending() {
+ return Object.keys(this._pending).length;
+ },
+
+ _updateIndent: function() {
+ let context = this._view.get_pango_context();
+ let metrics = context.get_metrics(null, null);
+ let charWidth = Math.max(metrics.get_approximate_char_width(),
+ metrics.get_approximate_digit_width());
+ let pixelWidth = Pango.units_to_double(charWidth);
+
+ let totalWidth = this._maxNickChars * pixelWidth + NICK_SPACING;
+
+ let tabs = Pango.TabArray.new(1, true);
+ tabs.set_tab(0, Pango.TabAlign.LEFT, totalWidth);
+ this._view.tabs = tabs;
+ this._view.indent = -totalWidth;
+ this._view.left_margin = MARGIN + totalWidth;
+ },
+
+ _ensureLogWalker: function() {
+ if (this._logWalker)
+ return;
+
+ let logManager = LogManager.getDefault();
+ this._logWalker = logManager.walkEvents(this._room.account,
+ this._room.channel_name);
+
+ this._fetchingBacklog = true;
+ this._logWalker.getEvents(NUM_INITIAL_LOG_EVENTS,
+ Lang.bind(this, this._onLogEventsReady));
+ },
+
+ _updateScroll: function() {
+ let adj = this.vadjustment;
+ if (adj.value == this._scrollBottom) {
+ if (this._nPending == 0) {
+ this._view.emit('move-cursor',
+ Gtk.MovementStep.BUFFER_ENDS, 1, false);
+ } else {
+ let id = Object.keys(this._pending).sort(function(a, b) {
+ return a - b;
+ })[0];
+ this._view.scroll_mark_onscreen(this._pending[id]);
+ }
+ }
+ this._scrollBottom = adj.upper - adj.page_size;
+ },
+
+ _onScroll: function(w, event) {
+ let [hasDir, dir] = event.get_scroll_direction();
+ if (hasDir && (dir != Gdk.ScrollDirection.UP || dir != Gdk.ScrollDirection.DOWN) )
+ return Gdk.EVENT_PROPAGATE;
+
+ let [hasDeltas, dx, dy] = event.get_scroll_deltas();
+ // print(dx, dy);
+ if (hasDeltas)
+ this._fetchBacklog();
+ // if (dir == Gdk.ScrollDirection.UP )
+ // print("UP");
+ // else if (dir == Gdk.ScrollDirection.DOWN)
+ // print("DOWN");
+
+ //return this._fetchBacklog();
+ },
+
+ _onKeyPress: function(w, event) {
+ let [, keyval] = event.get_keyval();
+
+ if (keyval === Gdk.KEY_Home ||
+ keyval === Gdk.KEY_KP_Home) {
+ this._view.emit('move-cursor',
+ Gtk.MovementStep.BUFFER_ENDS,
+ -1, false);
+ return Gdk.EVENT_STOP;
+ } else if (keyval === Gdk.KEY_End ||
+ keyval === Gdk.KEY_KP_End) {
+ this._view.emit('move-cursor',
+ Gtk.MovementStep.BUFFER_ENDS,
+ 1, false);
+ return Gdk.EVENT_STOP;
+ }
+
+ if (keyval != Gdk.KEY_Up &&
+ keyval != Gdk.KEY_KP_Up &&
+ keyval != Gdk.KEY_Page_Up &&
+ keyval != Gdk.KEY_KP_Page_Up)
+ return Gdk.EVENT_PROPAGATE;
+
+ return this._fetchBacklog();
+ },
+
+ _fetchBacklog: function() {
+ if (this.vadjustment.value != 0 &&
+ this.vadjustment.value != this._scrollBottom)
+ return Gdk.EVENT_PROPAGATE;
+
+ if (this._fetchingBacklog)
+ return Gdk.EVENT_STOP;
+
+ this._fetchingBacklog = true;
+
+ if (this.vadjustment.value == 0) {
+ this._showLoadingIndicator();
+ Mainloop.timeout_add(500, Lang.bind(this,
+ function() {
+ this._startQuery.next(10,this._cancellable,Lang.bind(this, this._onLogEventsReady));
+ }));
+ } else {
+ this._showLoadingIndicator1();
+ Mainloop.timeout_add(500, Lang.bind(this,
+ function() {
+ this._endQuery.next(10,this._cancellable,Lang.bind(this, this._onLogEventsReady1));
+ }));
+ }
+ return Gdk.EVENT_STOP;
+ },
+
+ _showUrlContextMenu: function(url, button, time) {
+ let menu = new Gtk.Menu();
+
+ let item = new Gtk.MenuItem({ label: _("Open Link") });
+ item.connect('activate', function() {
+ Utils.openURL(url, Gtk.get_current_event_time());
+ });
+ menu.append(item);
+
+ item = new Gtk.MenuItem({ label: _("Copy Link Address") });
+ item.connect('activate',
+ function() {
+ let clipboard = Gtk.Clipboard.get_default(item.get_display());
+ clipboard.set_text(url, -1);
+ });
+ menu.append(item);
+
+ menu.show_all();
+ menu.popup(null, null, null, button, time);
+ },
+
+ _showLoadingIndicator: function() {
+ let indicator = new Gtk.Image({ icon_name: 'content-loading-symbolic',
+ visible: true });
+
+ let buffer = this._view.buffer;
+ let iter = buffer.get_start_iter();
+ let anchor = buffer.create_child_anchor(iter);
+ this._view.add_child_at_anchor(indicator, anchor);
+ buffer.insert(iter, '\n', -1);
+
+ let start = buffer.get_start_iter();
+ buffer.remove_all_tags(start, iter);
+ buffer.apply_tag(this._lookupTag('loading'), start, iter);
+ },
+
+ _hideLoadingIndicator: function() {
+ let buffer = this._view.buffer;
+ let iter = buffer.get_start_iter();
+
+ if (!iter.get_child_anchor())
+ return;
+
+ iter.forward_line();
+ buffer.delete(buffer.get_start_iter(), iter);
+ },
+
+ _showLoadingIndicator1: function() {
+ let indicator = new Gtk.Image({ icon_name: 'content-loading-symbolic',
+ visible: true });
+
+ let buffer = this._view.buffer;
+ let iter = buffer.get_end_iter();
+ buffer.insert(iter, '\n', -1);
+ let anchor = buffer.create_child_anchor(iter);
+ this._view.add_child_at_anchor(indicator, anchor);
+
+ let end = buffer.get_end_iter();
+ iter.backward_line();
+ buffer.remove_all_tags(iter, end);
+ buffer.apply_tag(this._lookupTag('loading'), iter, end);
+ },
+
+ _hideLoadingIndicator1: function() {
+ let buffer = this._view.buffer;
+ let iter = buffer.get_end_iter();
+ // if (!iter.get_child_anchor())
+ // return;
+
+ iter.backward_line();
+ buffer.delete(iter, buffer.get_end_iter());
+ },
+
+ _insertMessage: function(iter, message, state) {
+ let isAction = message.messageType == Tp.ChannelTextMessageType.ACTION;
+ let needsGap = message.nick != state.lastNick || isAction;
+ let isCentre = message.id == this._uid;
+
+ if (message.timestamp - TIMESTAMP_INTERVAL > state.lastTimestamp) {
+ let tags = [this._lookupTag('timestamp')];
+ if (needsGap)
+ tags.push(this._lookupTag('gap'));
+ needsGap = false;
+ this._insertWithTags(iter,
+ Utils.formatTimestamp(message.timestamp) + '\n',
+ tags);
+ }
+ state.lastTimestamp = message.timestamp;
+
+// this._updateMaxNickChars(message.nick.length);
+
+ let tags = [];
+ if (isAction) {
+ message.text = "%s %s".format(message.nick, message.text);
+ state.lastNick = null;
+ tags.push(this._lookupTag('action'));
+ if (needsGap)
+ tags.push(this._lookupTag('gap'));
+ } else {
+ if (state.lastNick != message.nick) {
+ let tags = [this._lookupTag('nick')];
+ let nickTagName = this._getNickTagName(message.nick);
+ let nickTag = this._lookupTag(nickTagName);
+
+ if (!nickTag) {
+ nickTag = new Gtk.TextTag({ name: nickTagName });
+ this._view.get_buffer().get_tag_table().add(nickTag);
+ }
+ tags.push(nickTag);
+ if (needsGap)
+ tags.push(this._lookupTag('gap'));
+ this._insertWithTags(iter, message.nick + '\t', tags);
+ }
+ state.lastNick = message.nick;
+ tags.push(this._lookupTag('message'));
+ }
+
+ if (message.shouldHighlight)
+ tags.push(this._lookupTag('highlight'));
+
+ if (isCentre) {
+ let buffer = this._view.get_buffer();
+ buffer.create_mark('centre', iter, true);
+ }
+
+
+ // let params = this._room.account.dup_parameters_vardict().deep_unpack();
+ // let server = params.server.deep_unpack();
+
+ let text = message.text;
+ let res = [], match;
+ while ((match = this._keyregExp.exec(text))){
+ res.push({ keyword: match[0], pos: match.index});
+ }
+ // let channels = Utils.findChannels(text, server);
+ // let urls = Utils.findUrls(text).concat(channels).sort((u1,u2) => u1.pos - u2.pos);
+ let pos = 0;
+ for (let i = 0; i < res.length; i++) {
+ let cur = res[i];
+ this._insertWithTags(iter, text.substr(pos, cur.pos - pos), tags);
+
+ // let tag = this._createUrlTag(url.url);
+ // this._view.get_buffer().tag_table.add(tag);
+
+ // let name = url.name ? url.name : url.url;
+ this._insertWithTags(iter, cur.keyword,
+ tags.concat(this._lookupTag('highlight')));
+
+ pos = cur.pos + cur.keyword.length;
+ }
+ this._insertWithTags(iter, text.substr(pos), tags);
+ },
+
+ _createUrlTag: function(url) {
+ if (url.indexOf(':') == -1)
+ url = 'http://' + url;
+
+ let tag = new ButtonTag();
+ tag.connect('notify::hover', Lang.bind(this,
+ function() {
+ tag.foreground_rgba = tag.hover ? this._hoveredLinkColor : null;
+ }));
+ tag.connect('clicked',
+ function() {
+ Utils.openURL(url, Gtk.get_current_event_time());
+ });
+ tag.connect('button-press-event', Lang.bind(this,
+ function(tag, event) {
+ let [, button] = event.get_button();
+ if (button != Gdk.BUTTON_SECONDARY)
+ return Gdk.EVENT_PROPAGATE;
+
+ this._showUrlContextMenu(url, button, event.get_time());
+ return Gdk.EVENT_STOP;
+ }));
+ return tag;
+ },
+
+ _ensureNewLine: function() {
+ let buffer = this._view.get_buffer();
+ let iter = buffer.get_end_iter();
+ let tags = [];
+ let groupTag = this._lookupTag('status' + this._state.lastStatusGroup);
+ if (groupTag && iter.ends_tag(groupTag))
+ tags.push(groupTag);
+ let headerTag = this._lookupTag('status-compressed' + this._state.lastStatusGroup);
+ if (headerTag && iter.ends_tag(headerTag))
+ tags.push(headerTag);
+ if (iter.get_line_offset() != 0)
+ this._insertWithTags(iter, '\n', tags);
+ },
+
+ _getLineIters: function(iter) {
+ let start = iter.copy();
+ start.backward_line();
+ start.forward_to_line_end();
+
+ let end = iter.copy();
+ end.forward_to_line_end();
+
+ return [start, end];
+ },
+
+ _lookupTag: function(name) {
+ return this._view.get_buffer().tag_table.lookup(name);
+ },
+
+ _insertWithTagName: function(iter, text, name) {
+ this._insertWithTags(iter, text, [this._lookupTag(name)]);
+ },
+
+ _insertWithTags: function(iter, text, tags) {
+ let buffer = this._view.get_buffer();
+ let offset = iter.get_offset();
+
+ buffer.insert(iter, text, -1);
+
+ let start = buffer.get_iter_at_offset(offset);
+
+ buffer.remove_all_tags(start, iter);
+ for (let i = 0; i < tags.length; i++)
+ buffer.apply_tag(tags[i], start, iter);
+ },
+
+ _getNickTagName: function(nick) {
+ return NICKTAG_PREFIX + Polari.util_get_basenick(nick);
+ },
+
+ _setNickStatus: function(nick, status) {
+ let nickTag = this._lookupTag(this._getNickTagName(nick));
+ if (!nickTag)
+ return;
+
+ if (status == Tp.ConnectionPresenceType.AVAILABLE)
+ nickTag.foreground_rgba = this._activeNickColor;
+ else
+ nickTag.foreground_rgba = this._inactiveNickColor;
+
+ nickTag._status = status;
+ }
+});
diff --git a/src/searchManager.js b/src/searchManager.js
new file mode 100644
index 0000000..e69de29
diff --git a/src/utils.js b/src/utils.js
index edb9e5d..abeabad 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -109,6 +109,23 @@ function getURISchemes() {
return schemes;
}
+function initActions(actionMap, simpleActionEntries, context) {
+ simpleActionEntries.forEach(function(actionEntry) {
+ let props = {};
+ ['name', 'state', 'parameter_type'].forEach(
+ function(prop) {
+ if (actionEntry[prop])
+ props[prop] = actionEntry[prop];
+ });
+ let action = new Gio.SimpleAction(props);
+ if (actionEntry.create_hook)
+ actionEntry.create_hook(action);
+ if (actionEntry.activate)
+ action.connect('activate', actionEntry.activate);
+ });
+}
+
+
function getTpEventTime() {
let time = Gtk.get_current_event_time ();
if (time == 0)
@@ -255,3 +272,82 @@ function imgurPaste(pixbuf, title, callback) {
callback(null);
});
}
+
+function formatTimestamp(timestamp) {
+ let date = GLib.DateTime.new_from_unix_local(timestamp);
+ let now = GLib.DateTime.new_now_local();
+
+ // 00:01 actually, just to be safe
+ let todayMidnight = GLib.DateTime.new_local(now.get_year(),
+ now.get_month(),
+ now.get_day_of_month(),
+ 0, 1, 0);
+ let dateMidnight = GLib.DateTime.new_local(date.get_year(),
+ date.get_month(),
+ date.get_day_of_month(),
+ 0, 1, 0);
+ let daysAgo = todayMidnight.difference(dateMidnight) / GLib.TIME_SPAN_DAY;
+
+ let format;
+ let desktopSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
+ let clockFormat = desktopSettings.get_string('clock-format');
+ let hasAmPm = date.format('%p') != '';
+
+ if (clockFormat == '24h' || !hasAmPm) {
+ if(daysAgo < 1) { // today
+ /* Translators: Time in 24h format */
+ format = _("%H\u2236%M");
+ } else if(daysAgo <2) { // yesterday
+ /* Translators: this is the word "Yesterday" followed by a
+ time string in 24h format. i.e. "Yesterday, 14:30" */
+ // xgettext:no-c-format
+ format = _("Yesterday, %H\u2236%M");
+ } else if (daysAgo < 7) { // this week
+ /* Translators: this is the week day name followed by a time
+ string in 24h format. i.e. "Monday, 14:30" */
+ // xgettext:no-c-format
+ format = _("%A, %H\u2236%M");
+ } else if (date.get_year() == now.get_year()) { // this year
+ /* Translators: this is the month name and day number
+ followed by a time string in 24h format.
+ i.e. "May 25, 14:30" */
+ // xgettext:no-c-format
+ format = _("%B %d, %H\u2236%M");
+ } else { // before this year
+ /* Translators: this is the month name, day number, year
+ number followed by a time string in 24h format.
+ i.e. "May 25 2012, 14:30" */
+ // xgettext:no-c-format
+ format = _("%B %d %Y, %H\u2236%M");
+ }
+ } else {
+ if(daysAgo < 1) { // today
+ /* Translators: Time in 12h format */
+ format = _("%l\u2236%M %p");
+ } else if(daysAgo <2) { // yesterday
+ /* Translators: this is the word "Yesterday" followed by a
+ time string in 12h format. i.e. "Yesterday, 2:30 pm" */
+ // xgettext:no-c-format
+ format = _("Yesterday, %l\u2236%M %p");
+ } else if (daysAgo < 7) { // this week
+ /* Translators: this is the week day name followed by a time
+ string in 12h format. i.e. "Monday, 2:30 pm" */
+ // xgettext:no-c-format
+ format = _("%A, %l\u2236%M %p");
+ } else if (date.get_year() == now.get_year()) { // this year
+ /* Translators: this is the month name and day number
+ followed by a time string in 12h format.
+ i.e. "May 25, 2:30 pm" */
+ // xgettext:no-c-format
+ format = _("%B %d, %l\u2236%M %p");
+ } else { // before this year
+ /* Translators: this is the month name, day number, year
+ number followed by a time string in 12h format.
+ i.e. "May 25 2012, 2:30 pm"*/
+ // xgettext:no-c-format
+ format = _("%B %d %Y, %l\u2236%M %p");
+ }
+ }
+
+ return date.format(format);
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]