[polari/wip/kunaaljain/squashed-branch] Working Prototype



commit 6970820b9437ecdaa9b0f25d5d551dec30089ebc
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                        |   62 ++-
 src/org.gnome.Polari.src.gresource.xml   |    3 +
 src/resultList.js                        |  370 ++++++++++++
 src/resultStack.js                       |   49 ++
 src/resultView.js                        |  895 ++++++++++++++++++++++++++++++
 src/utils.js                             |   96 ++++
 12 files changed, 1876 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..a639598 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('(sussu)') }
         ];
         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..53ae406 100644
--- a/src/mainWindow.js
+++ b/src/mainWindow.js
@@ -8,6 +8,7 @@ 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;
@@ -15,9 +16,15 @@ const RoomList = imports.roomList;
 const RoomStack = imports.roomStack;
 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 +101,8 @@ const MainWindow = new Lang.Class({
     InternalChildren: ['titlebarRight',
                        'titlebarLeft',
                        'joinButton',
+                       'searchBar',
+                       'searchEntry',
                        'showUserListButton',
                        'userListPopover',
                        'roomListRevealer',
@@ -109,11 +118,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);
@@ -196,6 +211,18 @@ const MainWindow = new Lang.Class({
         this.connect('delete-event',
                             Lang.bind(this, this._onDelete));
 
+        // 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)
             this.set_default_size.apply(this, size);
@@ -214,6 +241,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 +277,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..fe88168
--- /dev/null
+++ b/src/resultList.js
@@ -0,0 +1,370 @@
+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.connect('key-press-event',
+                     Lang.bind(this, this._onKeyPress));
+
+    },
+
+    _onButtonRelease: function(w, event) {
+        let [, button] = event.get_button();
+        if (button != Gdk.BUTTON_SECONDARY)
+            return Gdk.EVENT_PROPAGATE;
+
+        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;
+
+
+        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._results = [];
+        this._widgetMap = {};
+        this._channelMap = {};
+
+        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('(sussu)', [row.uid, row.timestamp, row.channel, 
this._keywordsText, row.rank]));
+
+    },
+
+    _clearList: function() {
+        this.foreach(r => { r.hide(); });
+    },
+
+    _showList: function() {
+        this.foreach(r => { r.show(); });
+    },
+
+    _handleSearchChanged: function(group, actionName, value) {
+        this._cancellable.cancel();
+
+        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);
+                }
+
+                return GLib.SOURCE_REMOVE;
+            }));
+
+    },
+
+    _loadNextResults: function() {
+
+        if (this._fetchingResults)
+            return;
+
+        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];
+
+            if (row) {
+                widgetMap[uid] = row;
+                this.remove(row);
+            } else {
+                row = new ResultRow(events[i]);
+                widgetMap[uid] = row;
+            }
+
+            if( this._channelMap[events[i].chan] != null ) {
+
+                this._channelMap[events[i].chan]++;
+            } else {
+                this._channelMap[events[i].chan] = 0;
+            }
+            row.rank = this._channelMap[events[i].chan];
+            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);
+        }
+
+        // Select first result
+        if(events.length > 0) {
+            let row = this._widgetMap[events[0].id];
+        }
+
+        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;
+
+            if( this._channelMap[events[i].chan] ) {
+                this._channelMap[events[i].chan]++;
+            } else {
+                this._channelMap[events[i].chan] = 0;
+            }
+            row.rank = this._channelMap[events[i].chan];
+
+            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.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;
+
+        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 });
+
+
+        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 });
+
+
+        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);
+
+        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..ff81975
--- /dev/null
+++ b/src/resultStack.js
@@ -0,0 +1,49 @@
+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(channel, view) {
+        this._results[channel] = view;
+        this.add_named(view, channel);
+    },
+
+    _resultAdded: function(channel) {
+        this._addView(channel, new ResultView.ResultView(channel));
+    },
+
+    _resultRemoved: function(row) {
+        this._results[row.uid].destroy();
+        delete this._results[row.uid];
+    },
+
+    _activeResultChanged: function(action, parameter) {
+        let [uid, timestamp, channel, keywords, rank] = parameter.deep_unpack();
+
+        if(!this._results[channel])
+            this._resultAdded(channel);
+        this._results[channel]._insertView(uid, timestamp, rank);
+        this.set_visible_child_name(channel);
+    }
+});
diff --git a/src/resultView.js b/src/resultView.js
new file mode 100644
index 0000000..22294d5
--- /dev/null
+++ b/src/resultView.js
@@ -0,0 +1,895 @@
+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(channel) {
+        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 });
+
+        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._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._channelName = channel;
+        this._resultsAvailable = [];
+        this._seenMessages = {};
+
+        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.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);
+
+    },
+
+    _insertView: function(uid, timestamp, rank) {
+        let found = false;
+        let exists = false;
+        let startIndex = 0;
+
+        for(let i = 0; i < this._resultsAvailable.length; i++) {
+
+            if(this._resultsAvailable[i].rank > rank) {
+                found = true;
+                startIndex = i;
+            } else if(this._resultsAvailable[i].rank == rank) {
+                exists = true;
+                startIndex = i;
+            }
+        }
+
+
+        let buffer = this._view.buffer;
+        let iter = buffer.get_start_iter();
+        if(exists) {
+            let lastMark = buffer.get_mark('view-start' + this._resultsAvailable[startIndex].rank);
+            iter = buffer.get_iter_at_mark(lastMark);
+        } else if(found) {
+            let lastMark = buffer.get_mark('view-end' + this._resultsAvailable[startIndex].rank);
+            iter = buffer.get_iter_at_mark(lastMark);
+            buffer.delete_mark(lastMark);
+        }
+
+
+        if(!exists) {
+            let tags = [this._lookupTag('separator')];
+            this._insertWithTags(iter,'\n',tags);
+            buffer.create_mark('view-start' + rank, iter, true);
+            let obj = { top_query: null,
+                        bottom_query: null,
+                        rank: rank };
+
+            this._resultsAvailable.splice(startIndex + 1, 0, obj);
+
+
+            let rankTag = new Gtk.TextTag({ name: 'result'+rank, invisible: true });
+            this._view.get_buffer().get_tag_table().add(rankTag);
+        }
+
+        if(found && !exists) {
+            iter.backward_line();
+            buffer.create_mark('view-end' + this._resultsAvailable[startIndex].rank, iter, false);
+        }
+
+        if(!exists)
+            buffer.create_mark('view-end' + rank, iter, false);
+
+        let index;
+        for(let i = 0; i < this._resultsAvailable.length; i++) {
+            if(this._resultsAvailable[i].rank == rank) {
+                index = i;
+
+                rankTag = this._lookupTag('result'+this._resultsAvailable[i].rank);
+                rankTag.invisible = false;
+            } else {
+                rankTag = this._lookupTag('result'+this._resultsAvailable[i].rank);
+                rankTag.invisible = true;
+            }
+        }
+        this._rank = rank;
+        if(!exists) this._rowactivated(uid, this._channelName, timestamp, rank);
+        else {
+            this._startQuery = this._resultsAvailable[index].top_query;
+            this._endQuery = this._resultsAvailable[index].bottom_query;
+        }
+    },
+
+    _rowactivated: function(uid, channel, timestamp, rank) {
+        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);
+
+        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);
+
+        this._endQuery = new LogManager.GenericQuery(this._logManager._connection, 20);
+        this._endQuery.run(sparql,this._cancellable,Lang.bind(this, this._onLogEventsReady1, rank));
+
+        this._startQuery = new LogManager.GenericQuery(this._logManager._connection, 20);
+
+        this._startQuery.run(sparql1,this._cancellable,Lang.bind(this, this._onLogEventsReady, rank));
+        for(let i = 0; i < this._resultsAvailable.length; i++) {
+            if(this._resultsAvailable[i].rank == rank) {
+                index = i;
+                break;
+            }
+        }
+        this._resultsAvailable[index].top_query = this._startQuery;
+        this._resultsAvailable[index].bottom_query = this._endQuery;
+
+    },
+
+    _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 },
+          { name: 'separator',
+            invisible: true }
+        ];
+        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, rank) {
+
+        events = events.reverse();
+        this._hideLoadingIndicator();
+
+        this._pendingLogs = events.concat(this._pendingLogs);
+        this._insertPendingLogs(rank);
+        this._fetchingBacklog = false;
+    },
+
+    _onLogEventsReady1: function(events, rank) {
+
+        this._hideLoadingIndicator1();
+
+        this._pendingLogs = events.concat(this._pendingLogs);
+        this._insertPendingLogs1(rank);
+        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(rank) {
+        if (this._pendingLogs.length == 0)
+            return;
+
+
+        let nick = this._pendingLogs[0].sender;
+        let type = this._pendingLogs[0].messageType;
+
+        let pending = this._pendingLogs.splice(0);
+
+        let buffer = this._view.buffer;
+        let startMark = buffer.get_mark('view-start' + rank);
+        let iter = buffer.get_iter_at_mark(startMark);
+        let state = { lastNick: null, lastTimestamp: 0 };
+
+        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,
+                            rank: rank};
+            this._insertMessage(iter, message, state);
+            this._setNickStatus(message.nick, Tp.ConnectionPresenceType.OFFLINE);
+
+            if (!iter.is_end() || i < pending.length - 1) {
+                let tags = [this._lookupTag('result'+rank)];
+                this._insertWithTags(iter,'\n',tags);
+            }
+        }
+
+        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(rank) {
+        if (this._pendingLogs.length == 0)
+            return;
+
+        let index = -1;
+        let nick = this._pendingLogs[0].sender;
+        let type = this._pendingLogs[0].messageType;
+
+            index = 0;
+
+        let pending = this._pendingLogs.splice(index);
+
+        let state = { lastNick: null, lastTimestamp: 0 };
+        let buffer = this._view.buffer;
+        let endMark = buffer.get_mark('view-end' + rank);
+        let iter = buffer.get_iter_at_mark(endMark);
+        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,
+                            rank: rank};
+            this._insertMessage(iter, message, state);
+            this._setNickStatus(message.nick, Tp.ConnectionPresenceType.OFFLINE);
+
+            let tags = [this._lookupTag('result'+rank)];
+            this._insertWithTags(iter,'\n',tags);
+        }
+
+        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();
+
+        if (hasDeltas)
+            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, 
this._rank));
+                }));
+        } else {
+
+            this._showLoadingIndicator1();
+            Mainloop.timeout_add(500, Lang.bind(this,
+                function() {
+                    this._endQuery.next(10,this._cancellable,Lang.bind(this, this._onLogEventsReady1, 
this._rank));
+                }));
+        }
+        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();
+
+        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;
+        let rank = message.rank;
+
+        if (message.timestamp - TIMESTAMP_INTERVAL > state.lastTimestamp) {
+            let tags = [this._lookupTag('timestamp')];
+            tags.push(this._lookupTag('result'+rank));
+            if (needsGap)
+                tags.push(this._lookupTag('gap'));
+            needsGap = false;
+            this._insertWithTags(iter,
+                                 Utils.formatTimestamp(message.timestamp) + '\n',
+                                 tags);
+        }
+        state.lastTimestamp = message.timestamp;
+
+
+        let tags = [];
+        tags.push(this._lookupTag('result'+rank));
+        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')];
+                tags.push(this._lookupTag('result'+rank));
+                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 text = message.text;
+        let res = [], match;
+
+        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);
+
+            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]