Imported Upstream version 0.3~20101209
authorJulien Valroff <julien@kirya.net>
Thu, 9 Dec 2010 20:10:03 +0000 (21:10 +0100)
committerJulien Valroff <julien@kirya.net>
Thu, 9 Dec 2010 20:10:03 +0000 (21:10 +0100)
33 files changed:
.pc/.quilt_patches [deleted file]
.pc/.quilt_series [deleted file]
.pc/.version [deleted file]
CMakeLists.txt
config.h.in
img/conversation.png [new file with mode: 0644]
img/rt.png [new file with mode: 0644]
src/account_abstract.vala
src/accounts.vala
src/avatar.vala [new file with mode: 0644]
src/bg_box.vala [new file with mode: 0644]
src/content_view.vala
src/excluded/content_view.vala [new file with mode: 0644]
src/excluded/template.vala [new file with mode: 0644]
src/feed_model.vala [new file with mode: 0644]
src/feed_view.vala [new file with mode: 0644]
src/globals.vala
src/img_cache.vala
src/main.vala
src/main_window.vala
src/status.vala
src/status_box.vala
src/status_choose_bar.vala
src/status_delegate.vala [new file with mode: 0644]
src/stream_abstract.vala
src/template.vala
src/tree_widget.vala
src/twitter_account.vala
src/twitter_recursive_reply.vala [new file with mode: 0644]
src/twitter_stream_home.vala
src/vapi/config.vapi
src/view_area.vala [new file with mode: 0644]
src/wrap_label.vala [new file with mode: 0644]

diff --git a/.pc/.quilt_patches b/.pc/.quilt_patches
deleted file mode 100644 (file)
index 6857a8d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-debian/patches
diff --git a/.pc/.quilt_series b/.pc/.quilt_series
deleted file mode 100644 (file)
index c206706..0000000
+++ /dev/null
@@ -1 +0,0 @@
-series
diff --git a/.pc/.version b/.pc/.version
deleted file mode 100644 (file)
index 0cfbf08..0000000
+++ /dev/null
@@ -1 +0,0 @@
-2
index 64f01f1..4df6e0f 100644 (file)
@@ -194,6 +194,8 @@ set(pino_IMAGES
   img/userpic.svg
   img/updating.png
   img/retweet.svg
+  img/rt.png
+  img/conversation.png
 )
 install(FILES ${pino_IMAGES} DESTINATION share/pino/icons)
 
index e96de2f..45a5db7 100644 (file)
@@ -32,6 +32,8 @@
 #define SERVICE_TWITTER_ICON "${CMAKE_INSTALL_PREFIX}/share/pino/icons/service-twitter.png"
 #define SERVICE_IDENTICA_ICON "${CMAKE_INSTALL_PREFIX}/share/pino/icons/service-identica.png"
 #define REPLY_PATH "${CMAKE_INSTALL_PREFIX}/share/pino/icons/reply.png"
+#define RT_PATH "${CMAKE_INSTALL_PREFIX}/share/pino/icons/rt.png"
+#define CONVERSATION_PATH "${CMAKE_INSTALL_PREFIX}/share/pino/icons/conversation.png"
 #define RETWEET_PATH "${CMAKE_INSTALL_PREFIX}/share/pino/icons/retweet.svg"
 #define DELETE_PATH "${CMAKE_INSTALL_PREFIX}/share/pino/icons/delete_status.png"
 #define USERPIC_PATH "${CMAKE_INSTALL_PREFIX}/share/pino/icons/userpic.svg"
diff --git a/img/conversation.png b/img/conversation.png
new file mode 100644 (file)
index 0000000..2a298b3
Binary files /dev/null and b/img/conversation.png differ
diff --git a/img/rt.png b/img/rt.png
new file mode 100644 (file)
index 0000000..990709d
Binary files /dev/null and b/img/rt.png differ
index d97e8ea..ec59844 100644 (file)
@@ -13,6 +13,7 @@ public abstract class AAccount : GLib.Object {
        public signal void message_indicate(string msg);
        public signal void stop_indicate();
        public signal void do_reply(AAccount account, Status status);
+       public signal void insert_reply(string stream_hash, string status_id, Status result);
        
        /* For tree widget */
        public signal void stream_was_changed(AAccount account, AStream stream, int stream_index);
@@ -206,6 +207,7 @@ public abstract class AAccount : GLib.Object {
        }
        
        /** Return status from some stream */
+       /*
        protected Status? get_status(StreamEnum stype, string status_id) {
                foreach(AStream stream in streams) {
                        if(stream.stream_type != stype)
@@ -217,9 +219,10 @@ public abstract class AAccount : GLib.Object {
                }
                
                return null;
-       }
+       }*/
        
        /** Return all statuses with some id */
+       /*
        protected ArrayList<Status> get_statuses(string status_id) {
                ArrayList<Status> lst = new ArrayList<Status>();
                foreach(AStream stream in streams) {
@@ -229,9 +232,10 @@ public abstract class AAccount : GLib.Object {
                }
                
                return lst;
-       }
+       }*/
        
        /** Remove status from all streams */
+       /*
        protected void remove_status_complete(string status_id) {
                foreach(AStream stream in streams) {
                        Status? status = get_status_from_stream(stream, status_id);
@@ -259,7 +263,7 @@ public abstract class AAccount : GLib.Object {
                
                return null;
        }
-       
+       */
        /** Action from content view */
        public virtual AStream? new_content_action(string action_type,
                string stream_hash, string val) {
index fe9268d..8e15cf2 100644 (file)
@@ -6,7 +6,7 @@ public class Accounts : ArrayList<AAccount> {
        
        public signal void insert_new_account(AAccount account);
        public signal void insert_new_stream_after(string after_path, AStream stream);
-       public signal void element_was_removed(string path, AAccount account);
+       public signal void element_was_removed(string path, AAccount account, AStream? stream = null);
        public signal void fresh_items_changed(int items, string path);
        public signal void account_was_changed(string path, AAccount account);
        public signal void stream_was_changed(string path, AStream stream);
@@ -15,8 +15,7 @@ public class Accounts : ArrayList<AAccount> {
        public signal void message_indicate(string msg);
        public signal void stop_indicate();
        public signal void do_reply(AAccount account, Status status);
-       
-       //private ArrayList<AccountState> accounts_states;
+       public signal void insert_reply(string stream_hash, string status_id, Status result);
        
        private string accounts_path;
        
@@ -24,33 +23,6 @@ public class Accounts : ArrayList<AAccount> {
                base();
                
                init();
-               
-               /*string data = """<accounts>
-       <account type="TwitterAccount">
-               <s-name>testo</s-name>
-               <s-update-interval>3000</s-update-interval>
-               <streams>
-                       <stream type="0">
-                               <s-update-interval>5000</s-update-interval>
-                       </stream>
-                       <stream type="1">
-                               <s-update-interval>5000</s-update-interval>
-                       </stream>
-                       <stream type="0">
-                               <s-update-interval>5000</s-update-interval>
-                       </stream>
-               </streams>
-       </account>
-       <account type="TwitterAccount">
-               <s-name>trededed</s-name>
-               <s-update-interval>6000</s-update-interval>
-               <streams>
-                       <stream type="0">
-                               <s-update-interval>5000</s-update-interval>
-                       </stream>
-               </streams>
-       </account>
-</accounts>""";*/
        }
        
        private void init() {
@@ -339,6 +311,10 @@ public class Accounts : ArrayList<AAccount> {
                account.do_reply.connect((acc, status) => {
                        do_reply(acc, status);
                });
+               
+               account.insert_reply.connect((stream_hash, status_id, result) => {
+                       insert_reply(stream_hash, status_id, result);
+               });
        }
 
        /** New stream was added, we need to report about this to the tree widget */
@@ -350,7 +326,8 @@ public class Accounts : ArrayList<AAccount> {
        /** Stream was removed from some account */
        private void remove_stream(AAccount account, int stream_index) {
                int account_index = index_of(account);
-               element_was_removed("%d:%d".printf(account_index, stream_index), account);
+               AStream stream = account.streams.get(stream_index);
+               element_was_removed("%d:%d".printf(account_index, stream_index), account, stream);
        }
        
        /** New data in account */
diff --git a/src/avatar.vala b/src/avatar.vala
new file mode 100644 (file)
index 0000000..26490d4
--- /dev/null
@@ -0,0 +1,123 @@
+using Gtk;
+using Cairo;
+
+/** Special class for avatars. With rounded corners and shadows */
+public class Avatar : Image {
+       
+       public string url {get; set; default = "";}
+       public int pix_size {get; set; default = 1;}
+       
+       private const double M_PI = 3.1415926535;
+       private const double MAX_RGB = (double) uint16.MAX;
+       
+       public Avatar() {
+               GLib.Object();
+       }
+       
+       public Avatar.from_url(string url, int pix_size) {
+               this.url = url;
+               this.pix_size = pix_size;
+               load_pic();
+       }
+       
+       public void set_file_name(string file_name) {
+               //this.set_from_file(file_name);
+               pixbuf = img_cache.from_cache(file_name);
+               
+               if(pixbuf.width > pix_size || pixbuf.height > pix_size) {
+                       pixbuf = pixbuf.scale_simple(pix_size, pix_size, Gdk.InterpType.BILINEAR);
+               }
+               
+               redraw();
+       }
+       
+       public override bool expose_event(Gdk.EventExpose event) {
+               Context ctx = Gdk.cairo_create(this.window);
+               
+               if(pixbuf != null) {
+                       if(!pixbuf.has_alpha) {
+                               draw_rounded_path(ctx, allocation.x + 2, allocation.y + 2, allocation.width - 2,
+                                       allocation.height - 2, 4);
+                               ctx.set_source_rgb(242 / 256.0, 242 / 256.0, 242 / 256.0);
+                               ctx.fill_preserve();
+                               ctx.clip();
+                               
+                               ctx.reset_clip();
+                               
+                               draw_rounded_path(ctx, allocation.x + 1, allocation.y + 1, allocation.width - 2,
+                                       allocation.height - 2, 4);
+                               ctx.set_source_rgb(217 / 256.0, 217 / 256.0, 217 / 256.0);
+                               ctx.fill_preserve();
+                               ctx.clip();
+                               
+                               ctx.reset_clip();
+                       }
+                       
+                       draw_rounded_path(ctx, allocation.x, allocation.y, allocation.width - 2,
+                               allocation.height - 2, 4);
+                       
+                       Gdk.cairo_set_source_pixbuf(ctx, pixbuf, allocation.x, allocation.y);
+                       ctx.clip();
+                       ctx.paint();
+               }
+               return false;
+       }
+       
+       private void draw_rounded_path(Context ctx, double x, double y,
+               double width, double height, double radius) {
+               
+               double degrees = M_PI / 180.0;
+               
+               ctx.new_sub_path();
+               ctx.arc(x + width - radius, y + radius, radius, -90 * degrees, 0 * degrees);
+               ctx.arc(x + width - radius, y + height - radius, radius, 0 * degrees, 90 * degrees);
+               ctx.arc(x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees);
+               ctx.arc(x + radius, y + radius, radius, 180 * degrees, 270 * degrees);
+               ctx.close_path();
+       }
+       
+       private void set_color(Context ctx, Gdk.Color color) {
+               //get rgb
+               double r = (double) color.red / MAX_RGB;
+               double g = (double) color.green / MAX_RGB;
+               double b = (double) color.blue / MAX_RGB;
+               
+               ctx.set_source_rgb(r, g, b);
+       }
+       
+       private void redraw() {
+               if (null == this.window)
+                       return;
+
+               unowned Gdk.Region region = this.window.get_clip_region();
+               this.window.invalidate_region(region, true);
+               this.window.process_updates(true);
+    }
+    
+    public void load_pic() {
+               try {
+                       unowned Thread thread = Thread.create<void*>(load_pic_thread, true);
+               } catch(GLib.Error e) {
+                       debug(e.message); //TODO
+               }
+       }
+       
+       private void* load_pic_thread() {
+               string? img_path = img_cache.download(url);
+               
+               if(img_path != null) {
+                       //debug("%s, %s", img_path, status.user.pic);
+                       Idle.add(() => {
+                               try {
+                                       this.set_file_name(img_path);
+                               } catch(GLib.Error e) {
+                                       debug(e.message); //TODO
+                               }
+                               return false;
+                       });
+               }
+               
+               //debug("loading userpic");
+               return null;
+       }
+}
diff --git a/src/bg_box.vala b/src/bg_box.vala
new file mode 100644 (file)
index 0000000..4cbc54d
--- /dev/null
@@ -0,0 +1,71 @@
+using Gtk;
+using Cairo;
+
+
+/** Just for custom background */
+public class BgBox : HBox {
+       
+       public bool fresh {get; set; default = false;}
+       private const double MAX_RGB = (double) uint16.MAX;
+       
+       public BgBox(bool homogeneous, int spacing) {
+               GLib.Object(homogeneous: homogeneous, spacing: spacing);
+               
+               //when we fresh or not
+               notify["fresh"].connect((s) => {
+                       redraw();
+               });
+       }
+       
+       public override bool expose_event(Gdk.EventExpose event) {
+               if(fresh) {
+                       Context ctx = Gdk.cairo_create(this.window);
+                       
+                       Allocation alloc;
+                       get_allocation(out alloc);
+                       
+                       Gdk.cairo_rectangle(ctx, {0, 0, alloc.width, alloc.height});
+                       
+                       Cairo.Pattern grad = new Cairo.Pattern.linear(10, 0, 10, alloc.height);
+                       grad.add_color_stop_rgb(0, 1, 1, 1);
+                       grad.add_color_stop_rgb(1, 233/256.0, 249/256.0, 234/256.0);
+                       ctx.set_source(grad);
+                       
+                       ctx.fill();
+               } else {
+                       /*
+                       Context ctx = Gdk.cairo_create(this.window);
+                       //Gdk.Color color = Gdk.Color();
+                       //Gdk.Color.parse("blue", out color);
+                       
+                       //ctx.set_source_rgba(color.red / MAX_RGB, color.green / MAX_RGB,
+                       //              color.blue / MAX_RGB, 0.5);
+                       
+                       Allocation alloc;
+                       get_allocation(out alloc);
+                       
+                       Gdk.cairo_rectangle(ctx, {0, 0, alloc.width, alloc.height});
+                       
+                       Cairo.Pattern grad = new Cairo.Pattern.linear(10, 0, 10, alloc.height);
+                       grad.add_color_stop_rgb(0, 1, 1, 1);
+                       grad.add_color_stop_rgb(1, 233/256.0, 233/256.0, 233/256.0);
+                       ctx.set_source(grad);
+                       
+                       ctx.fill();
+                       */
+               }
+               
+               base.expose_event(event);
+               
+               return false;
+       }
+       
+       private void redraw() {
+               if (null == this.window)
+                       return;
+
+               unowned Gdk.Region region = this.window.get_clip_region ();
+               this.window.invalidate_region (region, true);
+               this.window.process_updates (true);
+    }
+}
index 7224263..d9f3986 100644 (file)
@@ -17,6 +17,8 @@ public class ContentView : GLib.Object {
        
        private string current_stream = "";
        
+       private bool not_more = false;
+       
        public ContentView(Accounts accounts, VisualStyle visual_style) {
                view = new WebView();
                view.set_size_request(250, 350);
@@ -36,9 +38,8 @@ public class ContentView : GLib.Object {
                scroll_map = new HashMap<string, string>(str_hash, str_equal);
                
                view.load_finished.connect((f) => {
-                       if(scroll_map.has_key(current_stream)) {
-                               slider.set_value(scroll_map[current_stream].to_double());
-                       }
+                       update_style();
+                       
                });
                
                //when scroll to the bottom
@@ -50,7 +51,32 @@ public class ContentView : GLib.Object {
                
                tpl = new Template(this.visual_style);
                
+               view.load_string(tpl.render_body(), "text/html", "utf8", "file:///");
+               
                this.accounts.stream_was_updated.connect(generate_list);
+               
+               this.accounts.insert_reply.connect(insert_reply);
+       }
+       
+       private void insert_reply(string stream_hash, string status_id, Status status) {
+               if(stream_hash != current_stream) {
+                       debug("not this stream");
+                       return;
+               }
+               
+               AStream? stream = accounts.stream_from_hash(stream_hash);
+               if(stream == null) {
+                       debug("can't find this stream");
+                       return;
+               }
+               
+               string result = tpl.render_small_status(status, stream);
+               result = result.replace("'", "\\'");
+               debug(result);
+               
+               string script = """insert_reply('%s', '%s');""".printf(status_id, result);
+               script = script.replace("\n", " ");
+               view.execute_script(script);
        }
        
        private void slider_move() {
@@ -58,7 +84,7 @@ public class ContentView : GLib.Object {
                double current = slider.get_value();
                double scroll_size = slider.adjustment.page_size;
                
-               if(current != 0 && current + scroll_size == max) {
+               if(!not_more && current != 0 && current + scroll_size == max) {
                        debug("need more");
                        AStream? stream = accounts.stream_from_hash(current_stream);
                        
@@ -75,7 +101,10 @@ public class ContentView : GLib.Object {
                if(stream.statuses.size == 0 && stream.statuses_fresh.size == 0)
                        return;
                
-               content_map.set(hash, tpl.stream_to_list(stream, hash));
+               string data = tpl.stream_to_list(stream, hash);
+               data = data.replace("\n", " ");
+               data = data.replace("'", "\\'");
+               content_map.set(hash, data);
                
                if(hash == current_stream)
                        set_current_list(hash);
@@ -85,29 +114,47 @@ public class ContentView : GLib.Object {
                if(hash == null)
                        return;
                
+               not_more = true;
+               
                if(current_stream != "")
                        scroll_map[current_stream] = slider.get_value().to_string();
                
                current_stream = hash;
                
                if(content_map.has_key(hash)) {
-                       view.load_string(content_map.get(hash), "text/html", "utf8", "file:///");
+                       //view.load_string(content_map.get(hash), "text/html", "utf8", "file:///");
+                       load_content(content_map.get(hash));
                } else {
-                       view.load_string("empty", "text/html", "utf8", "file:///");
+                       load_content("empty");
                }
                
-               debug(content_map.size.to_string());
+               if(scroll_map.has_key(current_stream)) {
+                       slider.set_value(scroll_map[current_stream].to_double());
+               }
+               
+               not_more = false;
+       }
+       
+       protected void load_content(owned string data) {
+               string script = """set_content('%s');""".printf(data);
+               view.execute_script(script);
        }
        
        private void update_style() {
                view.settings.set_property("default-font-size", visual_style.font_size);
                view.settings.set_property("default-font-family", visual_style.font_family);
                
-               tpl.render_header();
+               string header = tpl.render_header();
                
+               /*
                accounts.update_all_streams();
                
                set_current_list(current_stream);
+               */
+               
+               string script = """change_style("%s");""".printf(header);
+               script = script.replace("\n", " ");
+               view.execute_script(script);
                
                debug("style changed");
        }
diff --git a/src/excluded/content_view.vala b/src/excluded/content_view.vala
new file mode 100644 (file)
index 0000000..d9f3986
--- /dev/null
@@ -0,0 +1,186 @@
+using Gtk;
+using WebKit;
+using Gee;
+
+public class ContentView : GLib.Object {
+       
+       private WebView? view;
+       private ScrolledWindow scroll;
+       protected VScrollbar slider;
+       public Frame frame;
+       private Accounts accounts;
+       private VisualStyle visual_style;
+       private Template tpl;
+       
+       private HashMap<string, string> content_map;
+       private HashMap<string, string> scroll_map;
+       
+       private string current_stream = "";
+       
+       private bool not_more = false;
+       
+       public ContentView(Accounts accounts, VisualStyle visual_style) {
+               view = new WebView();
+               view.set_size_request(250, 350);
+               view.navigation_policy_decision_requested.connect(event_route);
+               view.settings.enable_default_context_menu = false;
+               
+               scroll = new ScrolledWindow(null, null);
+               scroll.set_policy(PolicyType.AUTOMATIC, PolicyType.AUTOMATIC);
+               scroll.add(view);
+               
+               slider = (VScrollbar)scroll.get_vscrollbar();
+               
+               frame = new Frame(null);
+               frame.add(scroll);
+               
+               content_map = new HashMap<string, string>(str_hash, str_equal);
+               scroll_map = new HashMap<string, string>(str_hash, str_equal);
+               
+               view.load_finished.connect((f) => {
+                       update_style();
+                       
+               });
+               
+               //when scroll to the bottom
+               slider.value_changed.connect(slider_move);
+               
+               this.accounts = accounts;
+               this.visual_style = visual_style;
+               this.visual_style.changed.connect(update_style);
+               
+               tpl = new Template(this.visual_style);
+               
+               view.load_string(tpl.render_body(), "text/html", "utf8", "file:///");
+               
+               this.accounts.stream_was_updated.connect(generate_list);
+               
+               this.accounts.insert_reply.connect(insert_reply);
+       }
+       
+       private void insert_reply(string stream_hash, string status_id, Status status) {
+               if(stream_hash != current_stream) {
+                       debug("not this stream");
+                       return;
+               }
+               
+               AStream? stream = accounts.stream_from_hash(stream_hash);
+               if(stream == null) {
+                       debug("can't find this stream");
+                       return;
+               }
+               
+               string result = tpl.render_small_status(status, stream);
+               result = result.replace("'", "\\'");
+               debug(result);
+               
+               string script = """insert_reply('%s', '%s');""".printf(status_id, result);
+               script = script.replace("\n", " ");
+               view.execute_script(script);
+       }
+       
+       private void slider_move() {
+               double max = slider.adjustment.upper;
+               double current = slider.get_value();
+               double scroll_size = slider.adjustment.page_size;
+               
+               if(!not_more && current != 0 && current + scroll_size == max) {
+                       debug("need more");
+                       AStream? stream = accounts.stream_from_hash(current_stream);
+                       
+                       if(stream == null)
+                               return;
+                       
+                       stream.menu_more();
+               }
+       }
+       
+       private void generate_list(string hash, AStream stream) {
+               debug(hash);
+               
+               if(stream.statuses.size == 0 && stream.statuses_fresh.size == 0)
+                       return;
+               
+               string data = tpl.stream_to_list(stream, hash);
+               data = data.replace("\n", " ");
+               data = data.replace("'", "\\'");
+               content_map.set(hash, data);
+               
+               if(hash == current_stream)
+                       set_current_list(hash);
+       }
+       
+       public void set_current_list(string? hash) {
+               if(hash == null)
+                       return;
+               
+               not_more = true;
+               
+               if(current_stream != "")
+                       scroll_map[current_stream] = slider.get_value().to_string();
+               
+               current_stream = hash;
+               
+               if(content_map.has_key(hash)) {
+                       //view.load_string(content_map.get(hash), "text/html", "utf8", "file:///");
+                       load_content(content_map.get(hash));
+               } else {
+                       load_content("empty");
+               }
+               
+               if(scroll_map.has_key(current_stream)) {
+                       slider.set_value(scroll_map[current_stream].to_double());
+               }
+               
+               not_more = false;
+       }
+       
+       protected void load_content(owned string data) {
+               string script = """set_content('%s');""".printf(data);
+               view.execute_script(script);
+       }
+       
+       private void update_style() {
+               view.settings.set_property("default-font-size", visual_style.font_size);
+               view.settings.set_property("default-font-family", visual_style.font_family);
+               
+               string header = tpl.render_header();
+               
+               /*
+               accounts.update_all_streams();
+               
+               set_current_list(current_stream);
+               */
+               
+               string script = """change_style("%s");""".printf(header);
+               script = script.replace("\n", " ");
+               view.execute_script(script);
+               
+               debug("style changed");
+       }
+       
+       private bool event_route(WebFrame p0, NetworkRequest request,
+               WebNavigationAction action, WebPolicyDecision decision) {
+               if(request.uri == "")
+                       return false;
+               
+               string prot = request.uri.split("://")[0];
+               string path = request.uri.split("://")[1];
+               debug(prot);
+               debug(path);
+               
+               if(prot == "file")
+                       return false;
+               
+               if(prot == "http" || prot == "https" || prot == "ftp") {
+                       GLib.Pid pid;
+                       GLib.Process.spawn_async(".", {"/usr/bin/xdg-open", request.uri}, null,
+                               GLib.SpawnFlags.STDOUT_TO_DEV_NULL, null, out pid);
+                       return true;
+               }
+               
+               accounts.new_content_action(prot, path);
+               
+               return true;
+       }
+}
diff --git a/src/excluded/template.vala b/src/excluded/template.vala
new file mode 100644 (file)
index 0000000..f27d42d
--- /dev/null
@@ -0,0 +1,628 @@
+using Gee;
+using PinoEnums;
+
+public class Template : Object {
+       
+       private VisualStyle visual_style;
+       
+       private string main_tpl = """
+               <html>
+                       <head>
+                       <script type="text/javascript">
+                       function insertAfter(newElement,targetElement) {
+                               var parent = targetElement.parentNode;
+                               if(parent.lastchild == targetElement) {
+                                       parent.appendChild(newElement);
+                               } else {
+                                       parent.insertBefore(newElement, targetElement.nextSibling);
+                               }
+                       }
+                       function insert_reply(status_id, data) {
+                               var footer = document.getElementById("footer" + status_id);
+                               footer.removeAttribute("href");
+                               
+                               var reply = document.getElementById("reply" + status_id);
+                               
+                               if(reply == null) {
+                                       reply = document.createElement("div");
+                                       reply.setAttribute("class", "reply-box");
+                                       reply.setAttribute("id", "reply" + status_id);
+                               }
+                               
+                               reply.innerHTML += data;
+                               
+                               var status = document.getElementById("status" + status_id);
+                               //alert(status);
+                               insertAfter(reply, status);
+                       }
+                       function change_style(data) {
+                               document.getElementById("style").innerHTML = data;
+                       }
+                       function set_content(data) {
+                               document.getElementById("body").innerHTML = data;
+                       }
+                       function menu(e, data) {
+                               if(e.button == 2) {
+                                       location.href="contextmenu://" + data;
+                                       return true;
+                               }
+                       }
+                       function reply(e, data) {
+                               var sel = window.getSelection();
+                               sel.removeAllRanges();
+                               location.href="reply://" + data;
+                               return true;
+                       }
+                       </script>
+                       <style type="text/css" id="style">
+                       </style>
+                       </head>
+                       <body id="body">
+                       %s
+                       </body>
+               </html>
+       """;
+       
+       /*
+       private string header_tpl = """
+               <style type="text/css">
+       body {
+               color: {{fg_color}};
+               #font-family: Droid Sans;
+               #font-size: 9pt;
+       }
+       .status, .status-fresh {
+               margin-bottom: 10px;
+       }
+       .tri {
+               z-index: 3;
+               position: absolute;
+               top: 16px;
+               left: 0px;
+               width: 14px;
+               height: 14px;
+               background-color: {{bg_color}};
+               border: 1px solid #ddd;
+               border-right-style: none;
+               border-top-style: none;
+               -webkit-transform: rotate(45deg);
+               -webkit-border-radius: 0px 0px 0px 2px;
+               -webkit-box-shadow: 0px 1px 1px  #ccc;
+       }
+       .line {
+               z-index: 5;
+               position: absolute;
+               background-color: {{bg_color}};
+               top: 14px;
+               left: 7px;
+               width: 1px;
+               height: 19px;
+               -webkit-border-radius: 3px;
+       }
+       .status-content {
+               z-index: 4;
+               position: relative;
+               background-color: {{bg_color}};
+               border: 1px solid #ddd;
+               -webkit-border-radius: 3px;
+               padding: 6px;
+               margin-left: 7px;
+               -webkit-box-shadow: 1px 1px 1px  #ccc;
+               cursor: default;
+       }
+       a {
+               color: {{lk_color}};
+       }
+       .tags {
+               font-weight: bold;
+               text-decoration: none;
+       }
+       .status-fresh .status-content {
+               border-width: 2px;
+               border-color: #478bde;
+       }
+       .status-fresh .tri {
+               border-width: 2px;
+               border-color: #478bde;
+       }
+       .status-fresh .line {
+               top: 16px;
+               height: 16px;
+               width: 2px;
+       }
+       .status-own .tri {
+               position: relative;
+               float: right;
+               -webkit-border-radius: 0px 2px 0px 0px;
+               -webkit-box-shadow: 1px 0px 1px  #ccc;
+       }
+       .status-own .line {
+               position: relative;
+               float: right;
+               width: 3px;
+               left: 10px;
+               #background-color: red;
+       }
+       .status-own .status-content {
+               margin-left: 0px;
+               margin-right: 7px;
+       }
+       .status-own .right {
+               margin-left: 0px;
+               margin-right: 58px;
+       }
+       .status-own .left {
+               float: right;
+       }
+       .right {
+               position:relative;
+               margin-bottom: 10px;
+               margin-left: 58px;
+       }
+       .left {
+               float: left;
+               width: 48px;
+               height: 48px;
+               backgrond-color: #fff;
+               -webkit-border-radius: 3px;
+               -webkit-background-size: 48px 48px;
+               -webkit-box-shadow: 1px 1px 1px  #ccc;
+       }
+       .header {
+               margin-bottom: 3px;
+       }
+       .header a, .re_nick {
+               font-weight: bold;
+               text-decoration: none;
+               color: {{fg_color}};
+               text-shadow: 1px 1px 0 #fff;
+       }
+       .date, .footer {
+               font-size: smaller;
+               font-weight: bold;
+               text-shadow: 1px 1px 0 #fff;
+               opacity: 0.6;
+               float: right;
+       }
+       .footer {
+               float: none;
+               display: block;
+               text-decoration: none;
+               margin-top: 3px;
+               color: {{fg_color}};
+       }
+       .rt {
+               background-color: {{fg_color}};
+               color: {{bg_color}};
+               opacity: 0.6;
+               margin-right: 2px;
+               font-weight: bold;
+               padding-left: 3px;
+               padding-right: 3px;
+               -webkit-border-radius: 3px;
+       }
+       .menu {
+               background-color: {{fg_color}};
+               opacity: 0.0;
+               width: 15px;
+               height: 15px;
+               float: right;
+               #margin-left: 3px;
+               margin-top: -9px;
+               #margin-bottom: 5px;
+               margin-right: -6px;
+               -webkit-border-radius: 3px 0 3px 0;
+       }
+       @-webkit-keyframes menu-hover {
+               from {
+                       opacity: 0.0;
+               }
+               to {
+                       opacity: 0.6;
+               }
+       }
+       .status-content:hover .menu {
+               opacity: 0.6;
+               -webkit-animation-name: menu-hover;
+               -webkit-animation-duration: 1s;
+       }
+               </style>
+       """;
+       */
+       
+       private string header_tpl = """
+       body {
+               color: {{fg_color}};
+               background: {{bg_color}};
+               #font-family: Droid Sans;
+               #font-size: 9pt;
+               margin: 0px;
+       }
+       .status, .status-fresh, .status-own, .status-small {
+               background: {{bg_light_color}};
+               padding: 6px;
+               position: relative;
+               min-height: 50px;
+               border: 0px solid #edeceb;
+               border-bottom-width: 1px;
+       }
+       .status-small {
+               border-left-width: 1px;
+               -webkit-border-radius: 3px 0px 0px 3px;
+               min-height: 30px;
+       }
+       .status-content {
+               z-index: 4;
+               position: relative;
+               margin-left: 7px;
+               cursor: default;
+       }
+       a {
+               color: {{lk_color}};
+       }
+       .tags {
+               font-weight: bold;
+               text-decoration: none;
+       }
+       .reply-box {
+               margin-left: 24px;
+       }
+       .status-fresh {
+               #background: #c3dff7;
+               background: -webkit-gradient(linear, 0 -75, 0 bottom, from({{bg_light_color}}), to(#c6ebb1));
+               border: 0px;
+       }
+       .status-own .status-content {
+               margin-left: 0px;
+               margin-right: 7px;
+       }
+       .status-own .right {
+               margin-left: 0px;
+               margin-right: 50px;
+       }
+       .status-own .left {
+               float: right;
+       }
+       .right {
+               position:relative;
+               margin-left: 50px;
+       }
+       .status-small .right {
+                       margin-left: 24px;
+       }
+       .left {
+               float: left;
+               width: 48px;
+               height: 48px;
+               backgrond-color: #fff;
+               -webkit-border-radius: 3px;
+               -webkit-background-size: 48px 48px;
+               -webkit-box-shadow: 1px 1px 1px  #ccc;
+       }
+       .status-small .left {
+               width: 24px;
+               height: 24px;
+               -webkit-background-size: 24px 24px;
+       }
+       .header {
+               margin-bottom: 3px;
+       }
+       .header a, .re_nick {
+               font-weight: bold;
+               text-decoration: none;
+               color: #404040;
+               text-shadow: 1px 1px 0 #fff;
+       }
+       .date, .footer {
+               font-size: smaller;
+               font-weight: bold;
+               text-shadow: 1px 1px 0 #fff;
+               opacity: 0.6;
+               float: right;
+       }
+       .status-own .header a {
+               float: right;
+       }
+       .status-own .header .date {
+               float: none;
+       }
+       .footer {
+               float: none;
+               #display: block;
+               text-decoration: none;
+               padding-top: 5px;
+               color: #404040;
+       }
+       .sep{
+               height: 6px;
+       }
+       .rt {
+               background-color: #404040;
+               color: #FCFBFA;
+               opacity: 0.6;
+               margin-right: 2px;
+               font-weight: bold;Goldman Sachs
+               padding-left: 3px;
+               padding-right: 3px;
+               -webkit-border-radius: 3px;
+       }
+       .menu {
+               background-color: #404040;
+               opacity: 0.0;
+               width: 15px;
+               height: 15px;
+               float: right;
+               margin-top: -9px;
+               margin-right: -6px;
+               -webkit-border-radius: 3px 0 3px 0;
+       }
+       .clear {
+               clear: both;
+       }
+       @-webkit-keyframes menu-hover {
+               from {
+                       opacity: 0.0;
+               }
+               to {
+                       opacity: 0.6;
+               }
+       }
+       .status-content:hover .menu {
+               opacity: 0.6;
+               -webkit-animation-name: menu-hover;
+               -webkit-animation-duration: 1s;
+       }
+       """;
+       
+       /*
+       private string status_tpl = """
+       <div class="status{{status_state}}">
+               <div class="left" style="background-image:url('{{user_pic}}');"></div>
+               <div class="right">
+                       <div class="tri"></div>
+                       <div class="line"></div>
+                       <div class="status-content">
+                       <div class="header">
+                               {{retweet}} <a href="">{{user_name}}</a>
+                               <span class="date">about one hour ago</span>
+                       </div>
+                       <div>{{content}}</div>
+                       {{footer}}
+                       <a href=""><div class="menu"></div></a>
+                       </div>
+               </div>
+       </div>
+       """;
+       */
+       
+       private string status_tpl = """
+       <div class="status{{status_state}}" id="status{{status_id}}" onmouseup="menu(event, '{{account_hash}}##{{stream_hash}}##{{status_id}}');" ondblclick="reply(event, '{{account_hash}}##{{stream_hash}}##{{status_id}}');">
+               <div class="left" style="background-image:url('{{user_pic}}');"></div>
+               <div class="right">
+                       <div class="status-content">
+                               <div class="header">
+                                       {{retweet}} <a href="">{{user_name}}</a>
+                                       <span class="date">{{created}}</span>
+                               </div>
+                               <div>{{content}}</div>
+                               {{footer}}
+                       </div>
+               </div>
+               <div class="clear"></div>
+       </div>
+       """;
+       
+       private string status_small_tpl = """
+       <div class="status-small">
+               <div class="left" style="background-image:url({{user_pic}});"></div>
+               <div class="right">
+                       <div class="status-content">
+                               <a class="re_nick" href="">{{user_name}}</a>: {{content}}
+                       </div>
+               </div>
+               <div class="clear"></div>
+       </div>
+       """;
+       
+       private string retweet_tpl = """<span class="rt">Rt:</span>""";
+       private string footer_tpl = """<div class="sep"></div><a class="footer" id="footer{{status_id}}" href="context://{{account_hash}}##{{stream_hash}}##{{status_id}}">%s</a>""";
+       
+       private string header;
+       
+       private Regex nicks;
+       private Regex tags;
+       private Regex groups;
+       private Regex urls;
+       private Regex clear_notice;
+       
+       public Template(VisualStyle visual_style) {
+               this.visual_style = visual_style;
+               
+               render_header();
+               
+               nicks = new Regex("(^|\\s|['\"+&!/\\(-])@([A-Za-z0-9_]+)");
+               tags = new Regex("(^|\\s|['\"+&!/\\(-])#([A-Za-z0-9_.-\\p{Latin}\\p{Greek}]+)");
+               groups = new Regex("(^|\\s|['\"+&!/\\(-])!([A-Za-z0-9_]+)"); //for identi.ca groups
+               urls = new Regex("((https?|ftp)://([A-Za-z0-9+&@#/%?=~_|!:,.;-]*)([A-Za-z0-9+&@#/%=~_|$]))"); // still needs to be improved for urls containing () such as wikipedia's
+               
+               // characters must be cleared to know direction of text
+               clear_notice = new Regex("[: \n\t\r♻♺]+|@[^ ]+");
+       }
+       
+       public string stream_to_list(AStream stream, string hash) {
+               //changing locale to C
+               string currentLocale = GLib.Intl.setlocale(GLib.LocaleCategory.TIME, null);
+               GLib.Intl.setlocale(GLib.LocaleCategory.TIME, "C");
+               
+               string result = "";
+               
+               foreach(Status status in stream.statuses_fresh) {
+                       result += render_fresh_status(status, stream);
+               }
+               
+               foreach(Status status in stream.statuses) {
+                       result += render_status(status, stream);
+               }
+               
+               //string main_result = main_tpl.printf(header, result);
+               //debug(main_result);
+               
+               //back to the normal locale
+               GLib.Intl.setlocale(GLib.LocaleCategory.TIME, currentLocale);
+               
+               return result;
+       }
+       
+       public string render_body() {
+               return main_tpl.printf("");
+       }
+       
+       public string render_header() {
+               HashMap<string, string> map = new HashMap<string, string>();
+               map["fg_color"] = visual_style.fg_color;
+               map["bg_color"] = visual_style.bg_color;
+               map["bg_light_color"] = visual_style.bg_light_color;
+               map["lk_color"] = visual_style.lk_color;
+               header = render(header_tpl, map);
+               return header;
+       }
+       
+       public string render_fresh_status(Status status, AStream stream) {
+               HashMap<string, string> map = new HashMap<string, string>();
+               map["status_state"] = "-fresh";
+               return render_status(status,stream,  map);
+       }
+       
+       public string render_status(Status status,
+               AStream stream, HashMap<string, string> map = new HashMap<string, string>()) {
+               
+               Status wstatus = status;
+               map["retweet"] = "";
+               map["footer"] = "";
+               
+               if(status.retweet != null) { //if this status is retweet
+                       wstatus = status.retweet;
+                       map["retweet"] = retweet_tpl;
+                       
+                       string re_by = _("retweeted by ") + status.user.name;
+                       map["footer"] = footer_tpl.printf(re_by);
+               }
+               
+               if(status.reply != null) { //if we have reply here
+                       string reply_to = _("in reply to ") + status.reply.name;
+                       map["footer"] = footer_tpl.printf(reply_to);
+               }
+               
+               if(!map.has_key("status_state")) //if not fresh
+                       map["status_state"] = "";
+               
+               if(wstatus.own) //if it your own status
+                       map["status_state"] = "-own";
+               
+               if(img_cache.exist(wstatus.user.pic)) //load from cache, if exist
+                       map["user_pic"] = img_cache.download(wstatus.user.pic);
+               else
+                       map["user_pic"] = wstatus.user.pic;
+               
+               map["user_name"] = wstatus.user.name;
+               
+               bool is_search = false;
+               if(stream.stream_type == StreamEnum.SEARCH)
+                       is_search = true;
+               
+               map["created"] = time_to_human_delta(wstatus.created, is_search);
+               
+               map["content"] = format_content(wstatus.content, stream);
+               
+               //context menu data
+               map["account_hash"] = stream.account.get_hash();
+               map["stream_hash"] = stream.account.get_stream_hash(stream);
+               map["status_id"] = status.id;
+               
+               return render(status_tpl, map);
+       }
+       
+       public string render_small_status(Status status, AStream stream) {
+               HashMap<string, string> map = new HashMap<string, string>();
+               if(img_cache.exist(status.user.pic)) //load from cache, if exist
+                       map["user_pic"] = img_cache.download(status.user.pic);
+               else
+                       map["user_pic"] = status.user.pic;
+               
+               
+               map["user_name"] = status.user.name;
+               map["content"] = format_content(status.content, stream);
+               
+               return render(status_small_tpl, map);
+       }
+       
+       private string render(string text, HashMap<string, string> map) {
+               string result = text;
+               
+               foreach(string key in map.keys) {
+                       var pat = new Regex("{{" + key + "}}");
+                       result = pat.replace(result, -1, 0, map[key]);
+               }
+               //debug(result);
+               return result;
+       }
+       
+       /* Performaing to show in html context */
+       private string strip_tags_plus(owned string content) {
+               content = content.replace("\\", "&#92;");
+               //content = Markup.escape_text(content);
+               content = content.replace("<", "&lt;");
+               content = content.replace(">", "&gt;");
+               
+               return content;
+       }
+       
+       private string format_content(owned string data, AStream stream) {
+               data = strip_tags_plus(data);
+               
+               string tmp = data;
+               
+               int pos = 0;
+               while(true) {
+                       //url cutting
+                       MatchInfo match_info;
+                       bool bingo = urls.match_all_full(tmp, -1, pos, GLib.RegexMatchFlags.NEWLINE_ANY, out match_info);
+                       if(bingo) {
+                               foreach(string s in match_info.fetch_all()) {
+                                       if(s.length > 30) {
+                                               data = data.replace(s, """<a href="%s" title="%s">%s...</a>""".printf(s, s, s.substring(0, 30)));
+                                       } else {
+                                               data = data.replace(s, """<a href="%s">%s</a>""".printf(s, s));
+                                       }
+                                       
+                                       match_info.fetch_pos(0, null, out pos);
+                                       break;
+                               }
+                       } else break;
+               }
+               
+               data = nicks.replace(data, -1, 0, "\\1@<a class='re_nick' href='userinfo://\\2'>\\2</a>");
+               data = tags.replace(data, -1, 0, "\\1#<a class='tags' href='search://%s::\\2'>\\2</a>".printf(stream.account.get_stream_hash(stream)));
+               
+               data = groups.replace(data, -1, 0, "\\1!<a class='tags' href='http://identi.ca/group/\\2'>\\2</a>");
+               
+               return data;
+       }
+       
+       private string time_to_human_delta(string created, bool is_search = false) {
+               int delta = TimeParser.time_to_diff(created, is_search);
+               
+               if(delta < 30)
+                       return _("a few seconds ago");
+               if(delta < 120)
+                       return _("1 minute ago");
+               if(delta < 3600)
+                       return _("%i minutes ago").printf(delta / 60);
+               if(delta < 7200)
+                       return _("about 1 hour ago");
+               if(delta < 86400)
+                       return _("about %i hours ago").printf(delta / 3600);
+               
+               return TimeUtils.str_to_time(created).format("%k:%M %b %d %Y");
+       }
+}
diff --git a/src/feed_model.vala b/src/feed_model.vala
new file mode 100644 (file)
index 0000000..ec4398b
--- /dev/null
@@ -0,0 +1,49 @@
+using Gee;
+
+/** List, that can connect to some feed view */
+public class FeedModel : ArrayList<Status> {
+       
+       public signal void status_added(Status status);
+       public signal void status_inserted(int index, Status status);
+       public signal void status_removed(int index);
+       
+       public override bool add(Status status) {
+               bool answer = base.add(status);
+               
+               status_added(status); //emit
+               
+               return answer;
+       }
+       
+       public override void insert(int index, Status status) {
+               base.insert(index, status);
+               status_inserted(index, status); //emit
+       }
+       
+       public override bool add_all(Collection<Status> lst) {
+               int i = 0;
+               foreach(Status status in lst) {
+                       insert(i, status);
+                       i += 1;
+               }
+               
+               return true;
+       }
+       
+       public override bool remove(Status status) {
+               int index = index_of(status);
+               bool answer = base.remove(status);
+               
+               status_removed(index); //emit
+               
+               return answer;
+       }
+       
+       public override Status remove_at(int index) {
+               Status status = base.remove_at(index);
+               
+               status_removed(index); //emit
+               
+               return status;
+       }
+}
diff --git a/src/feed_view.vala b/src/feed_view.vala
new file mode 100644 (file)
index 0000000..745c0d8
--- /dev/null
@@ -0,0 +1,51 @@
+using Gtk;
+
+/** Here we view all our statuses from one feed */
+public class FeedView : ScrolledWindow {
+       
+       private VBox vbox;
+       private FeedModel model;
+       
+       public FeedView() {
+               set_policy(PolicyType.AUTOMATIC, PolicyType.ALWAYS);
+               
+               vbox = new VBox(false, 2);
+               add_with_viewport(vbox);
+       }
+       
+       public void set_model(FeedModel model) {
+               this.model = model;
+               
+               foreach(Status status in model) {
+                       add_item(status);
+               }
+               
+               this.model.status_added.connect((status) => { add_item(status); });
+               this.model.status_inserted.connect(insert_item);
+               this.model.status_removed.connect(remove_item);
+       }
+       
+       private StatusDelegate new_delegate(Status status) {
+               StatusDelegate widget = new StatusDelegate(status);
+               widget.show_all();
+               debug("ok");
+               return widget;
+       }
+       
+       public StatusDelegate add_item(Status status) {
+               StatusDelegate widget = new_delegate(status);
+               vbox.pack_start(widget, false, false, 0);
+               
+               return widget;
+       }
+       
+       public void insert_item(int index, Status status) {
+               StatusDelegate widget = add_item(status);
+               vbox.reorder_child(widget, index);
+       }
+       
+       public void remove_item(int index) {
+               Widget widget = vbox.get_children().nth_data(index);
+               vbox.remove(widget);
+       }
+}
index 72c2c27..74ffc67 100644 (file)
@@ -1,4 +1,5 @@
 public AccountsTypes accounts_types;
 public StreamsTypes streams_types;
 public ImgCache img_cache;
-public Settings settings;
\ No newline at end of file
+public Settings settings;
+public VisualStyle visual_style;
index d16826e..11d88ed 100644 (file)
@@ -1,9 +1,11 @@
 using Soup;
+using Gee;
 
 public class ImgCache : Object {
        
        private Regex url_re;
        private string cache_path;
+       private HashMap<string, Gdk.Pixbuf> map;
        
        construct {
                try {
@@ -23,6 +25,24 @@ public class ImgCache : Object {
                                debug(e.message); //TODO
                        }
                }
+               
+               map = new HashMap<string, Gdk.Pixbuf>();
+       }
+       
+       private void load_pix(string path) {
+               if(!map.has_key(path)) {
+                       map[path] = new Gdk.Pixbuf.from_file(path);
+               }
+               
+               if(map[path].width > 48 || map[path].height > 48) {
+                       map[path] = map[path].scale_simple(48, 48, Gdk.InterpType.BILINEAR);
+               }
+       }
+       
+       public Gdk.Pixbuf? from_cache(string path) {
+               load_pix(path);
+               
+               return map[path];
        }
        
        public bool exist(string url) {
@@ -64,6 +84,8 @@ public class ImgCache : Object {
                if(!Soppa.save_soup_data(msg.response_body, new_path))
                        return null;
                
+               load_pix(new_path);
+               
                return new_path;
        }
        
index 999fe3c..e4c5c43 100644 (file)
@@ -1,5 +1,56 @@
 using Gtk;
 using Rest;
+using Cairo;
+
+public class TestWindow : Window {
+       
+       public TestWindow() {
+               set_default_size(300, 200);
+               
+               Status status1 = new Status();
+               status1.content = """This is a Vala port of the famous Egg Clock sample <a href="somelink"><b>@widget</b></a> using Cairo and GTK+ as <a href="link:action"><b>described</b></a> in the GNOME Journal: Part 1 and part 2""";
+               status1.user = new User();
+               status1.user.name = "SomeUser1";
+               status1.user.pic = "http://a2.twimg.com/profile_images/30581162/bobuk_normal.png";
+               
+               Status status2 = new Status();
+               status2.fresh = true;
+               status2.content = """This is a Vala port of the famous Egg Clock sample <a href="somelink"><b>@widget</b></a> using Cairo and GTK+ as <a href="link:action"><b>described</b></a> in the GNOME Journal: Part 1 and part 2""";
+               status2.user = new User();
+               status2.user.name = "SomeUser2";
+               status2.user.pic = "http://a0.twimg.com/profile_images/1139641176/omgubuntu_normal.png";
+               
+               Status status3 = new Status();
+               status3.content = """This is a Vala port of the famous Egg Clock sample <a href="somelink"><b>@widget</b></a> using Cairo and GTK+ as <a href="link:action"><b>described</b></a> in the GNOME Journal: Part 1 and part 2""";
+               status3.user = new User();
+               status3.user.name = "SomeUser3";
+               status3.user.pic = "http://a0.twimg.com/profile_images/185027712/_D0_A4_D0_B0_D0_B9_D0_BBTsar_nikolai_normal.jpg";
+               
+               Status status4 = new Status();
+               status4.fresh = true;
+               status4.content = """This is a Vala port of the famous Egg Clock sample <a href="somelink"><b>@widget</b></a> using Cairo and GTK+ as <a href="link:action"><b>described</b></a> in the GNOME Journal: Part 1 and part 2""";
+               status4.user = new User();
+               status4.user.name = "SomeUser4";
+               status4.user.pic = "http://a3.twimg.com/profile_images/1120466363/Clipboard02_normal.png";
+               
+               FeedModel model = new FeedModel();
+               model.add(status1);
+               model.add(status2);
+               model.add(status3);
+               
+               FeedView feed_view = new FeedView();
+               feed_view.set_model(model);
+               add(feed_view);
+               
+               show_all();
+               
+               model.insert(0, status4);
+               //model.remove_at(1);
+               
+               status4.fresh = false;
+               status3.fresh = true;
+       }
+}
 
 public static int main (string[] args) {
        Gtk.init (ref args);
@@ -15,6 +66,9 @@ public static int main (string[] args) {
                debug(e.message); //TODO
        }
        
+       //TestWindow w = new TestWindow();
+       
+       
        MainWindow win = new MainWindow();
        /*
        string api_key = "469089ec99372ee016bebd30218f1b23";
index 531ef63..b1b0af6 100644 (file)
@@ -9,9 +9,10 @@ public class MainWindow : Window {
        private Widget menubar;
        public MenuIndicator indicator;
        private TreeWidget tree;
-       private ContentView content_view;
+       //private ContentView content_view;
+       private ViewArea view_area;
        private StatusBox status_box;
-       private VisualStyle visual_style;
+       //private VisualStyle visual_style;
        private VPaned vpaned;
        private HPaned hpaned;
        
@@ -42,10 +43,12 @@ public class MainWindow : Window {
                
                visual_style = new VisualStyle(this);
                
-               content_view = new ContentView(accounts, visual_style);
+               //content_view = new ContentView(accounts, visual_style);
+               view_area = new ViewArea(accounts);
                
                HBox webbox = new HBox(false, 0);
-               webbox.pack_start(content_view.frame, true, true, 0);
+               //webbox.pack_start(content_view.frame, true, true, 0);
+               webbox.pack_start(view_area, true, true, 0);
                
                status_box = new StatusBox(this, accounts);
                
@@ -77,6 +80,7 @@ public class MainWindow : Window {
                
                //hide some widgets
                indicator.hide();
+               view_area.generate_views();
                
                signals_setup();
                
@@ -163,8 +167,9 @@ public class MainWindow : Window {
                        main_quit();
                });
                
-               tree.cursor_moved.connect((hash) => {
-                       content_view.set_current_list(hash);
+               tree.cursor_moved.connect((stream) => {
+                       //content_view.set_current_list(hash);
+                       view_area.set_current_view(stream);
                });
        }
 }
index 8fc9290..b886565 100644 (file)
@@ -1,5 +1,7 @@
 public class Status : GLib.Object {
        
+       public bool fresh {get; set; default = false;}
+       
        public string id {get; set; default = "";}
        public string content {get; set; default = "";}
        public bool own {get; set; default = false;}
index fbe846f..41a043c 100644 (file)
@@ -96,6 +96,17 @@ public class StatusBox : TextView {
        private void enter_pressed() {
                debug("enter");
                
+               if(settings.selected_for_posting.size < 1) {
+                       Gtk.MessageDialog dlg = new Gtk.MessageDialog(parent, Gtk.DialogFlags.MODAL,
+                               Gtk.MessageType.INFO, Gtk.ButtonsType.OK,
+                               _("You need to choose some accounts for sending statuses"));
+                       
+                       dlg.run();
+                       dlg.close();
+                       
+                       return;
+               }
+               
                vbox.set_sensitive(false);
                
                statuses_queue = settings.selected_for_posting.size;
index 40ea092..8d664a9 100644 (file)
@@ -33,7 +33,26 @@ public class StatusChooseBar : Toolbar {
                set_icon_size(IconSize.SMALL_TOOLBAR);
                toolbar_style = ToolbarStyle.ICONS;
                
-               this.accounts.element_was_removed.connect(remove_account);
+               accounts.insert_new_account.connect((acc) => {
+                       add_new_account(acc);
+                       update_config();
+               });
+               
+               accounts.element_was_removed.connect((path, acc) => {
+                       if(path.contains(":")) //just stream, not account
+                       return;
+               
+                       foreach(Widget tb in this.get_children()) {
+                               if(tb.get_type() != typeof(ToggleToolButton))
+                                       continue;
+                               
+                               if(((ToggleToolButton) tb).label == acc.get_hash()) {
+                                       this.remove(tb);
+                               }
+                       }
+                       
+                       update_config();
+               });
        }
        
        public void set_count(int chars) {
@@ -79,42 +98,41 @@ public class StatusChooseBar : Toolbar {
                }
        }
        
+       private void add_new_account(AAccount account) {
+               debug(account.s_name);
+               ToggleToolButton tb = new ToggleToolButton();
+               tb.label = account.get_hash();
+               tb.set_tooltip_text("%s (%s)".printf(account.s_name, account.id));
+               
+               if(settings.selected_for_posting.contains(tb.label))
+                       tb.set_active(true);
+               
+               account.notify["userpic"].connect((s) => {
+                       debug("userpic is loaded");
+                       Image img = new Image.from_pixbuf(account.userpic.scale_simple(
+                               24, 24, Gdk.InterpType.HYPER));
+                       img.show();
+                       tb.set_icon_widget(img);
+               });
+               
+               tb.toggled.connect(() => {
+                       update_config();
+               });
+               
+               /*
+               if(account.userpic != null) {
+                       debug("userpic is ok");
+                       Image img = new Image.from_pixbuf(account.userpic);
+                       img.pixel_size = 48;
+                       tb.set_icon_widget(img);
+               }*/
+               this.add(tb);
+               tb.show();
+       }
+       
        private void generate_acc() {
                foreach(AAccount account in this.accounts) {
-                       ToggleToolButton tb = new ToggleToolButton();
-                       tb.label = account.get_hash();
-                       tb.set_tooltip_text("%s (%s)".printf(account.s_name, account.id));
-                       
-                       if(settings.selected_for_posting.contains(tb.label))
-                               tb.set_active(true);
-                       
-                       account.notify["userpic"].connect((s) => {
-                               debug("userpic is loaded");
-                               Image img = new Image.from_pixbuf(account.userpic.scale_simple(
-                                       24, 24, Gdk.InterpType.HYPER));
-                               img.show();
-                               tb.set_icon_widget(img);
-                       });
-                       
-                       tb.toggled.connect(() => {
-                               update_config();
-                       });
-                       
-                       /*
-                       if(account.userpic != null) {
-                               debug("userpic is ok");
-                               Image img = new Image.from_pixbuf(account.userpic);
-                               img.pixel_size = 48;
-                               tb.set_icon_widget(img);
-                       }*/
-                       this.add(tb);
+                       add_new_account(account);
                }
        }
-       
-       private void remove_account(string path, AAccount account) {
-               if(path.contains(":")) //just stream, not account
-                       return;
-               
-               update_config();
-       }
 }
diff --git a/src/status_delegate.vala b/src/status_delegate.vala
new file mode 100644 (file)
index 0000000..14a21e5
--- /dev/null
@@ -0,0 +1,234 @@
+using Gtk;
+using Cairo;
+
+/** Separate class for status */
+public class StatusDelegate : EventBox {
+       
+       private Status? status;
+       
+       BgBox hb_main;
+       
+       private Avatar avatar;
+       private Label nick;
+       private Label date;
+       private WrapLabel content;
+       
+       private Gdk.Pixbuf? rt_pixbuf = null;
+       private const double MAX_RGB = (double) uint16.MAX;
+       
+       private Regex nicks;
+       private Regex tags;
+       private Regex groups;
+       private Regex urls;
+       private Regex clear_notice;
+       
+       public StatusDelegate(Status status) {
+               nicks = new Regex("(^|\\s|['\"+&!/\\(-])@([A-Za-z0-9_]+)");
+               tags = new Regex("(^|\\s|['\"+&!/\\(-])#([A-Za-z0-9_.-\\p{Latin}\\p{Greek}]+)");
+               groups = new Regex("(^|\\s|['\"+&!/\\(-])!([A-Za-z0-9_]+)"); //for identi.ca groups
+               urls = new Regex("((https?|ftp)://([A-Za-z0-9+&@#/%?=~_|!:,.;-]*)([A-Za-z0-9+&@#/%=~_|$]))"); // still needs to be improved for urls containing () such as wikipedia's
+               
+               // characters must be cleared to know direction of text
+               clear_notice = new Regex("[: \n\t\r♻♺]+|@[^ ]+");
+               
+               rt_pixbuf = new Gdk.Pixbuf.from_file(Config.RT_PATH);
+               
+               this.status = status;
+               
+               set_events(Gdk.EventMask.BUTTON_PRESS_MASK);
+               button_press_event.connect(on_click);
+               
+               hb_main = new BgBox(false, 0);
+               hb_main.fresh = status.fresh;
+               
+               //update background if fresh status changed
+               status.notify["fresh"].connect((s, p) => {
+                       hb_main.fresh = ((Status) s).fresh;
+               });
+               
+               VBox vb_avatar = new VBox(false, 0);
+               VBox vb_right = new VBox(false, 0);
+               HBox hb_header = new HBox(false, 0);
+               
+               //check if retweet
+               string av_url = "";
+               if(status.retweet == null)
+                       av_url = status.user.pic;
+               else
+                       av_url = status.retweet.user.pic;
+               
+               avatar = new Avatar.from_url(av_url, 48);
+               vb_avatar.pack_start(avatar, false, false, 4);
+               
+               //avatar.load_pic();
+               
+               //header
+               nick = new Label(null);
+               string user_name = "";
+               if(status.retweet == null)
+                       user_name = status.user.name;
+               else
+                       user_name = status.retweet.user.name;
+               
+               nick.set_markup("<b>%s</b>".printf(user_name));
+               date = new Label(null);
+               date.set_markup("<small><span foreground='#888'><b>%s</b></span></small>".printf(time_to_human_delta(status.created)));
+               
+               hb_header.pack_start(nick, false, false, 0);
+               hb_header.pack_end(date, false, false, 0);
+               
+               //content
+               content = new WrapLabel();
+               content.set_markup_plus(format_content(status.content));
+               
+               vb_right.pack_start(hb_header, false, false, 4);
+               vb_right.pack_start(content, false, false, 0);
+               
+               if(status.retweet != null) {
+                       HBox re_box = new HBox(false, 0);
+                       Label re_label = new Label(null);
+                       re_label.set_markup("<small><b><span foreground='#888'>retweeted by </span>%s</b></small>".printf(status.user.name));
+                       
+                       Avatar re_avatar = new Avatar.from_url(status.user.pic, 18);
+                       
+                       re_box.pack_start(re_avatar, false, false, 0);
+                       re_box.pack_start(re_label, false, false, 4);
+                       vb_right.pack_start(re_box, false, false, 4);
+               } else {
+                       if(status.reply != null) {
+                               HBox re_box = new HBox(false, 0);
+                               
+                               Image re_img = new Image.from_file(Config.CONVERSATION_PATH);
+                               Label re_label = new Label(null);
+                               re_label.set_markup("<small><b><span foreground='#888'>in reply to </span>%s</b></small>".printf(status.reply.name));
+                               
+                               re_box.pack_start(re_img, false, false, 0);
+                               re_box.pack_start(re_label, false, false, 4);
+                               vb_right.pack_start(re_box, false, false, 4);
+                       } else {
+                               HBox spacer = new HBox(false, 0);
+                               spacer.set_size_request(1, 4);
+                               vb_right.pack_start(spacer, false, false, 0);
+                       }
+               }
+               
+               hb_main.pack_start(vb_avatar, false, false, 4);
+               hb_main.pack_start(vb_right, true, true, 4);
+               
+               if(status.own) {
+                       hb_main.reorder_child(vb_right, 0);
+                       
+                       hb_header.remove(nick);
+                       hb_header.remove(date);
+                       hb_header.pack_start(date, false, false, 0);
+                       hb_header.pack_end(nick, false, false, 0);
+               }
+               
+               add(hb_main);
+               
+               //set bg color
+               Gdk.Color color = Gdk.Color();
+               Gdk.Color.parse("white", out color);
+               
+               modify_bg(StateType.NORMAL, color);
+       }
+       
+       /** Any click makes it not fresh */
+       private bool on_click(Gdk.EventButton event) {
+               if(!status.fresh)
+                       return true;
+               
+               status.fresh = false;
+               debug("ok");
+               
+               return false;
+       }
+       
+       /** Here we draw some things like retweet indicator and others */
+       public override bool expose_event(Gdk.EventExpose event) {
+               if(status == null || status.retweet == null)
+                       return base.expose_event(event);
+               
+               Context ctx = Gdk.cairo_create(this.window);
+               
+               bool answer = base.expose_event(event);
+               
+               if(rt_pixbuf != null) {
+                       Gdk.Rectangle big_rect = {0, 0 , 48, 48};
+                       Gdk.cairo_rectangle(ctx, big_rect);
+                       Gdk.cairo_set_source_pixbuf(ctx, rt_pixbuf, big_rect.x,
+                               big_rect.y);
+                       
+                       ctx.fill();
+               }
+               
+               return false;
+       }
+       
+       /** Convert status time to human readable string */
+       private string time_to_human_delta(string created, bool is_search = false) {
+               string currentLocale = GLib.Intl.setlocale(GLib.LocaleCategory.TIME, null);
+               GLib.Intl.setlocale(GLib.LocaleCategory.TIME, "C");
+               
+               int delta = TimeParser.time_to_diff(created, is_search);
+               
+               if(delta < 30)
+                       return _("a few seconds ago");
+               if(delta < 120)
+                       return _("1 minute ago");
+               if(delta < 3600)
+                       return _("%i minutes ago").printf(delta / 60);
+               if(delta < 7200)
+                       return _("about 1 hour ago");
+               if(delta < 86400)
+                       return _("about %i hours ago").printf(delta / 3600);
+               
+               GLib.Intl.setlocale(GLib.LocaleCategory.TIME, currentLocale);
+               
+               return TimeUtils.str_to_time(created).format("%k:%M %b %d %Y");
+       }
+       
+       /** Performaing to show in markup context */
+       private string strip_tags_plus(owned string content) {
+               //content = content.replace("\\", "&#92;");
+               content = Markup.escape_text(content);
+               //content = content.replace("<", "&lt;");
+               //content = content.replace(">", "&gt;");
+               
+               return content;
+       }
+       
+       private string format_content(owned string data) {
+               data = strip_tags_plus(data);
+               
+               string tmp = data;
+               
+               int pos = 0;
+               while(true) {
+                       //url cutting
+                       MatchInfo match_info;
+                       bool bingo = urls.match_all_full(tmp, -1, pos, GLib.RegexMatchFlags.NEWLINE_ANY, out match_info);
+                       if(bingo) {
+                               foreach(string s in match_info.fetch_all()) {
+                                       if(s.length > 30) {
+                                               data = data.replace(s, """<a href="%s" title="%s">%s...</a>""".printf(s, s, s.substring(0, 30)));
+                                       } else {
+                                               data = data.replace(s, """<a href="%s">%s</a>""".printf(s, s));
+                                       }
+                                       
+                                       match_info.fetch_pos(0, null, out pos);
+                                       break;
+                               }
+                       } else break;
+               }
+               debug(visual_style.fg_color);
+               data = nicks.replace(data, -1, 0, "\\1<b><a href='userinfo://\\2'><span foreground='=fg-color='>@\\2</span></a></b>");
+               debug(data);
+               //data.printf("foreground='#ccc'");
+               data = tags.replace(data, -1, 0, "\\1<b><a href='search://\\2'>#\\2</a></b>");
+               
+               data = groups.replace(data, -1, 0, "\\1<b>!<a href='http://identi.ca/group/\\2'>\\2</a></b>");
+               data = data.replace("=fg-color=", visual_style.fg_color);
+               return data;
+       }
+}
index eab9326..6bf8e86 100644 (file)
@@ -7,9 +7,11 @@ public abstract class AStream : Object {
        
        public signal void updated();
        
-       public ArrayList<Status> statuses {get; set; default = new ArrayList<Status>();}
+       //public ArrayList<Status> statuses {get; set; default = new ArrayList<Status>();}
        
-       public ArrayList<Status> statuses_fresh {get; set; default = new ArrayList<Status>();}
+       //public ArrayList<Status> statuses_fresh {get; set; default = new ArrayList<Status>();}
+       
+       public FeedModel model {get; set; default = new FeedModel();}
        
        public abstract StreamEnum stream_type {get;}
        
index 2655c4e..f27d42d 100644 (file)
@@ -9,6 +9,38 @@ public class Template : Object {
                <html>
                        <head>
                        <script type="text/javascript">
+                       function insertAfter(newElement,targetElement) {
+                               var parent = targetElement.parentNode;
+                               if(parent.lastchild == targetElement) {
+                                       parent.appendChild(newElement);
+                               } else {
+                                       parent.insertBefore(newElement, targetElement.nextSibling);
+                               }
+                       }
+                       function insert_reply(status_id, data) {
+                               var footer = document.getElementById("footer" + status_id);
+                               footer.removeAttribute("href");
+                               
+                               var reply = document.getElementById("reply" + status_id);
+                               
+                               if(reply == null) {
+                                       reply = document.createElement("div");
+                                       reply.setAttribute("class", "reply-box");
+                                       reply.setAttribute("id", "reply" + status_id);
+                               }
+                               
+                               reply.innerHTML += data;
+                               
+                               var status = document.getElementById("status" + status_id);
+                               //alert(status);
+                               insertAfter(reply, status);
+                       }
+                       function change_style(data) {
+                               document.getElementById("style").innerHTML = data;
+                       }
+                       function set_content(data) {
+                               document.getElementById("body").innerHTML = data;
+                       }
                        function menu(e, data) {
                                if(e.button == 2) {
                                        location.href="contextmenu://" + data;
@@ -22,9 +54,10 @@ public class Template : Object {
                                return true;
                        }
                        </script>
-                       %s
+                       <style type="text/css" id="style">
+                       </style>
                        </head>
-                       <body>
+                       <body id="body">
                        %s
                        </body>
                </html>
@@ -198,21 +231,26 @@ public class Template : Object {
        */
        
        private string header_tpl = """
-               <style type="text/css">
        body {
                color: {{fg_color}};
-               background: {{bg_light_color}}
+               background: {{bg_color}};
                #font-family: Droid Sans;
                #font-size: 9pt;
                margin: 0px;
        }
-       .status, .status-fresh, .status-own {
+       .status, .status-fresh, .status-own, .status-small {
+               background: {{bg_light_color}};
                padding: 6px;
                position: relative;
                min-height: 50px;
                border: 0px solid #edeceb;
                border-bottom-width: 1px;
        }
+       .status-small {
+               border-left-width: 1px;
+               -webkit-border-radius: 3px 0px 0px 3px;
+               min-height: 30px;
+       }
        .status-content {
                z-index: 4;
                position: relative;
@@ -226,6 +264,9 @@ public class Template : Object {
                font-weight: bold;
                text-decoration: none;
        }
+       .reply-box {
+               margin-left: 24px;
+       }
        .status-fresh {
                #background: #c3dff7;
                background: -webkit-gradient(linear, 0 -75, 0 bottom, from({{bg_light_color}}), to(#c6ebb1));
@@ -246,6 +287,9 @@ public class Template : Object {
                position:relative;
                margin-left: 50px;
        }
+       .status-small .right {
+                       margin-left: 24px;
+       }
        .left {
                float: left;
                width: 48px;
@@ -255,6 +299,11 @@ public class Template : Object {
                -webkit-background-size: 48px 48px;
                -webkit-box-shadow: 1px 1px 1px  #ccc;
        }
+       .status-small .left {
+               width: 24px;
+               height: 24px;
+               -webkit-background-size: 24px 24px;
+       }
        .header {
                margin-bottom: 3px;
        }
@@ -323,7 +372,7 @@ public class Template : Object {
                -webkit-animation-name: menu-hover;
                -webkit-animation-duration: 1s;
        }
-               </style>""";
+       """;
        
        /*
        private string status_tpl = """
@@ -347,7 +396,7 @@ public class Template : Object {
        */
        
        private string status_tpl = """
-       <div class="status{{status_state}}" onmouseup="menu(event, '{{account_hash}}##{{stream_hash}}##{{status_id}}');" ondblclick="reply(event, '{{account_hash}}##{{stream_hash}}##{{status_id}}');">
+       <div class="status{{status_state}}" id="status{{status_id}}" onmouseup="menu(event, '{{account_hash}}##{{stream_hash}}##{{status_id}}');" ondblclick="reply(event, '{{account_hash}}##{{stream_hash}}##{{status_id}}');">
                <div class="left" style="background-image:url('{{user_pic}}');"></div>
                <div class="right">
                        <div class="status-content">
@@ -363,8 +412,20 @@ public class Template : Object {
        </div>
        """;
        
+       private string status_small_tpl = """
+       <div class="status-small">
+               <div class="left" style="background-image:url({{user_pic}});"></div>
+               <div class="right">
+                       <div class="status-content">
+                               <a class="re_nick" href="">{{user_name}}</a>: {{content}}
+                       </div>
+               </div>
+               <div class="clear"></div>
+       </div>
+       """;
+       
        private string retweet_tpl = """<span class="rt">Rt:</span>""";
-       private string footer_tpl = """<div class="sep"></div><a class="footer" href="">%s</a>""";
+       private string footer_tpl = """<div class="sep"></div><a class="footer" id="footer{{status_id}}" href="context://{{account_hash}}##{{stream_hash}}##{{status_id}}">%s</a>""";
        
        private string header;
        
@@ -403,23 +464,27 @@ public class Template : Object {
                        result += render_status(status, stream);
                }
                
-               string main_result = main_tpl.printf(header, result);
+               //string main_result = main_tpl.printf(header, result);
                //debug(main_result);
                
                //back to the normal locale
                GLib.Intl.setlocale(GLib.LocaleCategory.TIME, currentLocale);
                
-               return main_result;
+               return result;
+       }
+       
+       public string render_body() {
+               return main_tpl.printf("");
        }
        
-       public void render_header() {
+       public string render_header() {
                HashMap<string, string> map = new HashMap<string, string>();
                map["fg_color"] = visual_style.fg_color;
                map["bg_color"] = visual_style.bg_color;
                map["bg_light_color"] = visual_style.bg_light_color;
                map["lk_color"] = visual_style.lk_color;
                header = render(header_tpl, map);
-               //debug(header);
+               return header;
        }
        
        public string render_fresh_status(Status status, AStream stream) {
@@ -477,6 +542,20 @@ public class Template : Object {
                return render(status_tpl, map);
        }
        
+       public string render_small_status(Status status, AStream stream) {
+               HashMap<string, string> map = new HashMap<string, string>();
+               if(img_cache.exist(status.user.pic)) //load from cache, if exist
+                       map["user_pic"] = img_cache.download(status.user.pic);
+               else
+                       map["user_pic"] = status.user.pic;
+               
+               
+               map["user_name"] = status.user.name;
+               map["content"] = format_content(status.content, stream);
+               
+               return render(status_small_tpl, map);
+       }
+       
        private string render(string text, HashMap<string, string> map) {
                string result = text;
                
@@ -484,6 +563,7 @@ public class Template : Object {
                        var pat = new Regex("{{" + key + "}}");
                        result = pat.replace(result, -1, 0, map[key]);
                }
+               //debug(result);
                return result;
        }
        
@@ -510,9 +590,9 @@ public class Template : Object {
                        if(bingo) {
                                foreach(string s in match_info.fetch_all()) {
                                        if(s.length > 30) {
-                                               data = data.replace(s, "<a href='%s' title='%s'>%s...</a>".printf(s, s, s.substring(0, 30)));
+                                               data = data.replace(s, """<a href="%s" title="%s">%s...</a>""".printf(s, s, s.substring(0, 30)));
                                        } else {
-                                               data = data.replace(s, "<a href='%s'>%s</a>".printf(s, s));
+                                               data = data.replace(s, """<a href="%s">%s</a>""".printf(s, s));
                                        }
                                        
                                        match_info.fetch_pos(0, null, out pos);
index 101497e..0350ab9 100644 (file)
@@ -3,7 +3,7 @@ using PinoEnums;
 
 public class TreeWidget : TreeView {
        
-       public signal void cursor_moved(string hash);
+       public signal void cursor_moved(AStream stream);
        
        private Window parent;
        private Accounts accounts;
@@ -124,7 +124,7 @@ public class TreeWidget : TreeView {
                        
                        string hash = active_account.get_stream_hash(active_stream);
 
-                       cursor_moved(hash);
+                       cursor_moved(active_stream);
                }
        }
 
@@ -298,7 +298,7 @@ public class TreeWidget : TreeView {
        }
        
        /** Remove stream or account */
-       private void remove_element(string path, AAccount account) {
+       private void remove_element(string path, AAccount account, AStream? stream = null) {
                TreeIter iter;
                store.get_iter_from_string(out iter, path);
                
index dd56403..af1656f 100644 (file)
@@ -61,6 +61,8 @@ public class Account : AAccount {
                }
        }
        
+       protected RecursiveReply rec_reply;
+       
        construct {
        }
        
@@ -147,6 +149,7 @@ public class Account : AAccount {
                        add_stream(StreamEnum.SEARCH, true, map);
                        return null;
                
+               /*
                case "reply":
                        Status? status = null;
                        foreach(AStream stream in streams) {
@@ -158,7 +161,33 @@ public class Account : AAccount {
                        if(status != null)
                                menu_do_reply(status);
                        break;
-               
+               */
+               /*
+               case "context":
+                       Status? status = null;
+                       foreach(AStream stream in streams) {
+                               if(get_stream_hash(stream) == stream_hash) {
+                                       status = get_status(stream.stream_type, val);
+                                       break;
+                               }
+                       }
+                       
+                       if(status == null)
+                               return null;
+                       
+                       debug(status.id);
+                       
+                       //get status and send signal to cintent_view
+                       rec_reply = new RecursiveReply(proxy, status, s_name,
+                               stream_hash);
+                       rec_reply.new_reply.connect((rstatus, shash, sid) => {
+                               insert_reply(shash, sid, rstatus); //signal
+                       });
+                       rec_reply.run();
+                       
+                       break;
+               */
+               /*
                case "contextmenu":
                        debug("menu");
                        AStream? stream = null;
@@ -191,6 +220,7 @@ public class Account : AAccount {
                                debug("can't find this status");
                        
                        return null;
+               */
                }
                
                return null;
@@ -255,6 +285,7 @@ public class Account : AAccount {
        }
        
        protected override void menu_do_favorite(Status status) {
+               /*
                Rest.ProxyCall call = proxy.new_call();
                call.set_method("POST");
                
@@ -286,6 +317,7 @@ public class Account : AAccount {
                });
                
                call.run_async(callback, this);
+               */
        }
        
        protected override void menu_do_retweet(Status status) {
@@ -308,6 +340,7 @@ public class Account : AAccount {
        }
        
        protected override void menu_do_remove(Status status) {
+               /*
                Rest.ProxyCall call = proxy.new_call();
                call.set_method("POST");
                
@@ -332,6 +365,7 @@ public class Account : AAccount {
                });
                
                call.run_async(callback, this);
+               */
        }
        
        protected override void menu_do_reply(Status status) {
diff --git a/src/twitter_recursive_reply.vala b/src/twitter_recursive_reply.vala
new file mode 100644 (file)
index 0000000..94994fa
--- /dev/null
@@ -0,0 +1,52 @@
+using RestCustom;
+
+namespace Twitter {
+
+public class RecursiveReply : GLib.Object  {
+       
+       public signal void new_reply(Status status, string stream_hash, string sid);
+       
+       private Rest.Proxy proxy;
+       private string s_name = "";
+       private Status fstatus;
+       private string stream_hash = "";
+       
+       public RecursiveReply(Rest.Proxy proxy, Status fstatus, string s_name,
+               string stream_hash) {
+               
+               this.proxy = proxy;
+               this.fstatus = fstatus;
+               this.s_name = s_name;
+               this.stream_hash = stream_hash;
+       }
+       
+       public void run() {
+               debug("ok");
+               get_reply(fstatus);
+       }
+       
+       private void get_reply(Status status) {
+               if(status.reply == null)
+                       return;
+               
+               Rest.ProxyCall call = proxy.new_call();
+               call.set_function("statuses/show/%s.xml".printf(status.reply.status_id));
+               call.set_method("GET");
+               debug("statuses/show/%s.xml".printf(status.id));
+               Rest.ProxyCallAsyncCallback callback = status_get_respose;
+               call.run_async(callback, this);
+       }
+       
+       protected void status_get_respose(Rest.ProxyCall call, Error? error, Object? obj) {
+               debug(call.get_status_code().to_string());
+               Status? status = Parser.get_status_from_string(call.get_payload(), s_name);
+               
+               if(status == null)
+                       return;
+               
+               new_reply(status, stream_hash, fstatus.id);
+               
+               get_reply(status);
+       }
+}
+}
index a05bcc0..eb78be6 100644 (file)
@@ -43,7 +43,7 @@ public class StreamHome : Twitter.StreamAbstract {
                        if(!more) {
                                call.add_param("since_id", s_last_id.to_string());
                        } else {
-                               call.add_param("max_id", statuses.get(statuses.size - 1).id);
+                               call.add_param("max_id", model.get(model.size - 1).id);
                        }
                }
        }
@@ -58,18 +58,26 @@ public class StreamHome : Twitter.StreamAbstract {
                }
                
                if(s_last_id == 0) {
-                       statuses.add_all(result_lst);
-                       s_last_id = statuses.get(0).id.to_int64();
+                       model.add_all(result_lst);
+                       s_last_id = model.get(0).id.to_int64();
                        
                        return;
                }
                
+               fresh_items = result_lst.size;
+               
                fresh_to_normal();
                
+               foreach(Status status in result_lst) {
+                       status.fresh = true;
+               }
+               
                //getting fresh statuses
-               statuses_fresh.add_all(result_lst);
-               s_last_id = statuses_fresh.get(0).id.to_int64();
+               model.add_all(result_lst);
+               
+               s_last_id = model.get(0).id.to_int64();
                
+               /*
                int own_statuses = 0;
                foreach(Status status in statuses_fresh) {
                        if(status.own)
@@ -77,7 +85,7 @@ public class StreamHome : Twitter.StreamAbstract {
                }
                
                fresh_items = statuses_fresh.size - own_statuses;
-               
+               */
                debug("What we got: %d", result_lst.size);
        }
        
@@ -85,19 +93,18 @@ public class StreamHome : Twitter.StreamAbstract {
                ArrayList<Status> result_lst = parsing_delegate(data, own_name);
                
                if(result_lst.size > 1) {
-                       statuses.add_all(result_lst.slice(2, result_lst.size - 1));
+                       //model.add_all(result_lst.slice(2, result_lst.size - 1));
+                       foreach(Status status in result_lst) {
+                               model.add(status);
+                       }
                }
        }
        
        /* Moving fresh statuses to the normal statuses */
        protected void fresh_to_normal() {
-               int i = 0;
-               foreach(Status status in statuses_fresh) {
-                       statuses.insert(i, status);
-                       i++;
+               foreach(Status status in model) {
+                       status.fresh = false;
                }
-               
-               statuses_fresh.clear();
        }
        
        public override void menu_more() {
index 9a97eed..1cb501d 100644 (file)
@@ -35,6 +35,8 @@ namespace Config
        public const string PUBLIC_TIMELINE_ICON;
        public const string DIRECT_PATH;
        public const string DIRECT_FRESH_PATH;
+       public const string RT_PATH;
+       public const string CONVERSATION_PATH;
        //public const string FAVORITE_PATH;
        //public const string FAVORITE_NO_PATH;
        public const string SERVICE_TWITTER_ICON;
diff --git a/src/view_area.vala b/src/view_area.vala
new file mode 100644 (file)
index 0000000..642be58
--- /dev/null
@@ -0,0 +1,87 @@
+using Gtk;
+using Gee;
+
+public class ViewArea : VBox {
+       
+       private Accounts accounts;
+       private HashMap<AStream, FeedView> map;
+       private AStream current_stream;
+       
+       public ViewArea(Accounts accounts) {
+               this.accounts = accounts;
+               
+               map = new HashMap<AStream, FeedView>();
+               
+               homogeneous = true;
+               spacing = 0;
+               
+               accounts.insert_new_stream_after.connect((path, stream) => {
+                       FeedView view = create_feed_view(stream);
+                       view.show_all();
+                       set_current_view(stream);
+               });
+               
+               accounts.element_was_removed.connect((path, account, stream) => {
+                       //if account
+                       if(stream == null) {
+                               foreach(AStream st in account.streams) {
+                                       remove_feed_view(st);
+                                       debug("streams removed");
+                               }
+                       } else { //if stream
+                               remove_feed_view(stream);
+                       }
+               });
+               
+               accounts.insert_new_account.connect((account) => {
+                       foreach(AStream stream in account.streams) {
+                               create_feed_view(stream);
+                       }
+               });
+               
+               //generate_views();
+               //show_all();
+       }
+       
+       public void generate_views() {
+               foreach(AAccount account in accounts) {
+                       foreach(AStream stream in account.streams) {
+                               create_feed_view(stream);
+                       }
+               }
+       }
+       
+       private void remove_feed_view(AStream stream) {
+               FeedView view = map[stream];
+               remove(view);
+               map.remove(stream);
+               
+               if(stream == current_stream)
+                       current_stream.dispose();
+       }
+       
+       private FeedView create_feed_view(AStream stream) {
+               FeedView view = new FeedView();
+               
+               view.set_model(stream.model);
+               
+               map[stream] = view;
+               
+               pack_start(view, true, true, 0);
+               view.hide();
+               
+               return view;
+       }
+       
+       public void set_current_view(AStream stream) {
+               if(stream == current_stream)
+                       return;
+               
+               map[current_stream].hide();
+               current_stream = stream;
+               
+               
+               FeedView view = map[stream];
+               view.show_all();
+       }
+}
diff --git a/src/wrap_label.vala b/src/wrap_label.vala
new file mode 100644 (file)
index 0000000..4b13cfe
--- /dev/null
@@ -0,0 +1,54 @@
+using Gtk;
+
+/** Label with NORMAL wrapping. Thanks to VMWare team */
+public class WrapLabel : Label {
+       
+       public int m_wrap_width = 0;
+       public int m_wrap_height = 0;
+       
+       public WrapLabel(string? str = null) {
+               get_layout().set_wrap(Pango.WrapMode.WORD_CHAR);
+               set_alignment(0, 0);
+               
+               set_text(str);
+               set_wrap_width(m_wrap_width);
+               
+               activate_link.connect(link_clicked);
+       }
+       
+       private bool link_clicked(string url) {
+               debug(url);
+               return true;
+       }
+       
+       private void set_wrap_width(int width) {
+               if (width == 0) {
+                       return;
+               }
+               
+               get_layout().set_width((int) (width * Pango.SCALE));
+               
+               int unused = 0;
+               get_layout().get_pixel_size(out unused, out m_wrap_height);
+               
+               if (m_wrap_width != width) {
+                       m_wrap_width = width;
+                       queue_resize();
+               }
+       }
+       
+       public override void size_request(out Gtk.Requisition req) {            
+               req.width  = 0;
+               req.height = m_wrap_height;
+       }
+       
+       public override void size_allocate(Gdk.Rectangle alloc) {
+               base.size_allocate(alloc);
+               set_wrap_width(alloc.width);
+       }
+       
+       public void set_markup_plus(string txt) {
+               set_markup(txt);
+               set_wrap_width(m_wrap_width);
+       }
+}