Commits

troorl committed 5e74093 Merge

2.85 aka 3.0.0-alpha1

Comments (0)

Files changed (31)

   img/userpic.svg
   img/updating.png
   img/retweet.svg
+  img/rt.png
+  img/conversation.png
 )
 install(FILES ${pino_IMAGES} DESTINATION share/pino/icons)
 
 #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"

img/conversation.png

Added
New image
Added
New image

src/account_abstract.vala

 	}
 	
 	/** Return status from some stream */
+	/*
 	protected Status? get_status(StreamEnum stype, string status_id) {
 		foreach(AStream stream in streams) {
 			if(stream.stream_type != stype)
 		}
 		
 		return null;
-	}
+	}*/
 	
 	/** Return all statuses with some id */
 	protected ArrayList<Status> get_statuses(string status_id) {
 			Status? status = get_status_from_stream(stream, status_id);
 			if(status != null)
 				lst.add(status);
+			
+			lst.add_all(get_child_statuses(stream, status_id));
 		}
 		
 		return lst;
 		foreach(AStream stream in streams) {
 			Status? status = get_status_from_stream(stream, status_id);
 			if(status != null) {
-				if(!stream.statuses_fresh.remove(status))
-					stream.statuses.remove(status);
+				stream.model.remove(status);
 				
-				stream.menu_refresh();
+				//stream.menu_refresh();
 			}
+			
+			//remove_child_statuses(stream, status_id);
 		}
 	}
 	
-	protected Status? get_status_from_stream(AStream stream, string status_id) {
-		foreach(Status status in stream.statuses_fresh) {
-			if(status.id == status_id) {
-				return status;
+	/* TODO
+	protected void remove_child_statuses(AStream stream, string status_id) {
+		foreach(Status status in stream.model) {
+			if(status.conversation != null) {
+				foreach(Status cstatus in status.conversation) {
+					if(cstatus.id == status_id)
+						status.conversation.remove(cstatus);
+				}
+			}
+		}
+	}*/
+	
+	protected ArrayList<Status> get_child_statuses(AStream stream, string status_id) {
+		ArrayList<Status> lst = new ArrayList<Status>();
+		
+		foreach(Status status in stream.model) {
+			if(status.conversation != null) {
+				foreach(Status cstatus in status.conversation) {
+					if(cstatus.id == status_id)
+						lst.add(cstatus);
+				}
 			}
 		}
 		
-		foreach(Status status in stream.statuses) {
+		return lst;
+	}
+	
+	protected Status? get_status_from_stream(AStream stream, string status_id) {
+		foreach(Status status in stream.model) {
 			if(status.id == status_id) {
 				return status;
 			}
 	}
 	
 	/** Show status context menu */
-	protected virtual void context_menu(AStream stream, Status status) {
+	public virtual void context_menu(AStream stream, Status status) {
 		Menu menu = new Menu();
 		debug("creating context menu");
 		
 		menu.show_all();
 	}
 	
+	public virtual void get_conversation(Status status) {}
+	
+	public virtual void go_hashtag(string tag) {}
+	
 	/** Virtual context menu actions */
 	protected virtual void menu_do_reply(Status status) {}
 	

src/accounts.vala

 	
 	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);
 	public signal void do_reply(AAccount account, Status status);
 	public signal void insert_reply(string stream_hash, string status_id, Status result);
 	
-	//private ArrayList<AccountState> accounts_states;
-	
 	private string accounts_path;
 	
 	public Accounts() {
 		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() {
 	/** 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 */
+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;
+	}
+}
+using Gtk;
+using Cairo;
+
+
+/** Just for custom background */
+public class BgBox : HBox {
+	
+	public bool fresh {get; set; default = false;}
+	public bool favorited {get; set; default = false;}
+	private const double MAX_RGB = (double) uint16.MAX;
+	
+	//it's not actualy true colors, like in Gdk
+	private Gdk.Color color_fresh;
+	private Gdk.Color color_favorited;
+	
+	public BgBox(bool homogeneous, int spacing) {
+		GLib.Object(homogeneous: homogeneous, spacing: spacing);
+		
+		color_fresh = {1, 233, 249, 234};
+		color_favorited = {1, 243, 237, 121};
+		
+		//when we fresh or not
+		notify["fresh"].connect((s) => {
+			redraw();
+		});
+		
+		//when we favorited or not
+		notify["favorited"].connect((s) => {
+			redraw();
+		});
+	}
+	
+	public override bool expose_event(Gdk.EventExpose event) {
+		if(fresh && !favorited)
+			draw_background(color_fresh);
+		
+		if(favorited)
+			draw_background(color_favorited);
+		
+		base.expose_event(event);
+		
+		return false;
+	}
+	
+	private void draw_background(Gdk.Color color) {
+		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, color.red/256.0, color.green/256.0, color.blue/256.0);
+		ctx.set_source(grad);
+		
+		ctx.fill();
+	}
+	
+	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);
+    }
+}

src/conversation_view.vala

+using Gtk;
+
+public class ConversationView : HBox {
+	
+	private VBox vbox;
+	private EventBox spacer;
+	
+	public ConversationView() {
+		GLib.Object(homogeneous: false, spacing: 0);
+		spacer = new EventBox();
+		spacer.set_size_request(20, 10);
+		vbox = new VBox(false, 2);
+		pack_start(spacer, false, false, 0);
+		pack_start(vbox, true, true, 0);
+	}
+	
+	public void add_delegate(StatusDelegate d) {
+		vbox.pack_start(d, true, true, 0);
+		d.show_all();
+	}
+}

src/event_box_tr.vala

+using Gtk;
+using Cairo;
+
+/** Class for event boxes with transparent background and 'hand' cursor on enter-motion event */
+public class EventBoxTr : EventBox {
+	
+	public EventBoxTr() {
+		GLib.Object();
+		
+		set_has_window(false);
+		
+		set_events(Gdk.EventMask.BUTTON_RELEASE_MASK);
+		set_events(Gdk.EventMask.ENTER_NOTIFY_MASK);
+		set_events(Gdk.EventMask.LEAVE_NOTIFY_MASK);
+		
+		enter_notify_event.connect((event) => {
+			set_has_window(true);
+			get_window().set_cursor(new Gdk.Cursor(Gdk.CursorType.HAND2));
+			return true;
+		});
+		
+		leave_notify_event.connect((event) => {
+			get_window().set_cursor(null);
+			set_has_window(false);
+			return true;
+		});
+	}
+}

src/excluded/content_view.vala

+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;
+	}
+}

src/excluded/template.vala

+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");
+	}
+}

src/feed_model.vala

+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, AStream stream);
+	public signal void status_removed(int index);
+	
+	public AStream? stream = null;
+	
+	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, stream); //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;
+	}
+}

src/feed_view.vala

+using Gtk;
+
+/** Here we view all our statuses from one feed */
+public class FeedView : ScrolledWindow {
+	
+	private VBox vbox;
+	private FeedModel model;
+	private VScrollbar scroll;
+	
+	public FeedView() {
+		set_policy(PolicyType.AUTOMATIC, PolicyType.ALWAYS);
+		
+		vbox = new VBox(false, 2);
+		add_with_viewport(vbox);
+		
+		scroll = (VScrollbar) get_vscrollbar();
+		scroll.value_changed.connect(() => {
+			//debug(scroll.get_value().to_string());
+			
+			double max = scroll.adjustment.upper;
+			double current = scroll.get_value();
+			double scroll_size = scroll.adjustment.page_size;
+			
+			if(current != 0 && current + scroll_size == max) {
+				debug("max");
+				if(model.stream != null) {
+					model.stream.menu_more();
+				}
+			}
+		});
+	}
+	
+	public void set_model(FeedModel model) {
+		this.model = model;
+		
+		foreach(Status status in model) {
+			add_item(status, model.stream);
+		}
+		
+		this.model.status_added.connect((status) => { add_item(status, model.stream); });
+		this.model.status_inserted.connect(insert_item);
+		this.model.status_removed.connect(remove_item);
+	}
+	
+	private StatusDelegate new_delegate(Status status, AStream stream) {
+		StatusDelegate widget = new StatusDelegate(status, stream);
+		widget.show_all();
+		debug("ok");
+		return widget;
+	}
+	
+	public StatusDelegate add_item(Status status, AStream stream) {
+		StatusDelegate widget = new_delegate(status, stream);
+		vbox.pack_start(widget, false, false, 0);
+		
+		return widget;
+	}
+	
+	public void insert_item(int index, Status status, AStream stream) {
+		StatusDelegate widget = add_item(status, stream);
+		vbox.reorder_child(widget, index);
+	}
+	
+	public void remove_item(int index) {
+		Widget widget = vbox.get_children().nth_data(index);
+		vbox.remove(widget);
+	}
+}
 public AccountsTypes accounts_types;
 public StreamsTypes streams_types;
 public ImgCache img_cache;
-public Settings settings;
+public Settings settings;
+public VisualStyle visual_style;
+public MainWindow main_window;

src/img_cache.vala

 using Soup;
+using Gee;
 
 public class ImgCache : Object {
 	
 	private Regex url_re;
 	private string cache_path;
+	private HashMap<string, Gdk.Pixbuf> map;
 	
 	construct {
 		try {
 				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) {
 		if(!Soppa.save_soup_data(msg.response_body, new_path))
 			return null;
 		
+		load_pix(new_path);
+		
 		return new_path;
 	}
 	
 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);
 		debug(e.message); //TODO
 	}
 	
-	MainWindow win = new MainWindow();
+	//TestWindow w = new TestWindow();
+	
+	
+	main_window = new MainWindow();
 	/*
 	string api_key = "469089ec99372ee016bebd30218f1b23";
 	string app_secret = "09c8836c79ba2f7182273bfb706c58c0";

src/main_window.vala

 	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;
 	
 		
 		menu_setup();
 		
-		indicator = new MenuIndicator();
+		indicator = new MenuIndicator(this);
 		
 		accounts.message_indicate.connect((msg) => {
 			indicator.add_queue(msg);
 		
 		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);
 		
 		
 		//hide some widgets
 		indicator.hide();
+		view_area.generate_views();
 		
 		signals_setup();
 		
 			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);
 		});
 	}
 }

src/menu_indicator.vala

 	
 	private Label tlabel;
 	private Spinner spin;
+	private Window main_window;
 	
-	public MenuIndicator() {
+	public MenuIndicator(Window main_window) {
+		this.main_window = main_window;
 		remove(get_child());
 		
 		tlabel = new Label("some text");
 	}
 	
 	public void add_queue(string text) {
+		main_window.get_window().set_cursor(new Gdk.Cursor(Gdk.CursorType.WATCH));
+		
 		show_all();
 		spin.start();
 		tlabel.set_text(text);
 	public void hide_queue() {
 		spin.stop();
 		hide_all();
+		
+		main_window.get_window().set_cursor(null);
 	}
 }

src/reply_label.vala

+using Gtk;
+
+public class ReplyLabel : HBox {
+	
+	private Label label;
+	private Image img;
+	private HBox hb;
+	private EventBoxTr ebox;
+	private Spinner? progress = null;
+	
+	public signal void clicked();
+	
+	public ReplyLabel(string username) {
+		GLib.Object(homogeneous: false, spacing: 0);
+		
+		ebox = new EventBoxTr();
+		hb = new HBox(false, 0);
+		
+		img = new Image.from_file(Config.CONVERSATION_PATH);
+		label = new Label(null);
+		label.set_markup("<small><b><span foreground='#888'>%s </span>%s</b></small>".printf(_("in reply to"), username));
+		
+		hb.pack_start(img, false, false, 0);
+		hb.pack_start(label, false, false, 4);
+		
+		ebox.add(hb);
+		
+		this.pack_start(ebox, false, false, 0);
+		
+		ebox.button_release_event.connect((event) => {
+			//debug("ok");
+			if(event.button == 1)
+				clicked(); //signal
+			
+			return true;
+		});
+	}
+	
+	public void set_tooltip(string text) {
+		hb.set_tooltip_text(text);
+	}
+	
+	public void start() {
+		progress = new Spinner();
+		progress.show();
+		progress.start();
+		hb.pack_start(progress, false, false, 0);
+	}
+	
+	public void stop() {
+		if(progress != null) {
+			hb.remove(progress);
+			//progress.expose();
+			progress = null;
+		}
+	}
+}
+using Gee;
+
 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;}
 	public bool favorited {get; set; default = false;}
 	public string created {get; set; default = "";}
 	public Reply? reply {get; set; default = null;}
+	
+	public ArrayList<Status>? conversation {get; set; default = null;}
+	
+	public signal void new_reply(Status status);
+	public signal void end_reply();
 }
 
 public class User : GLib.Object {

src/status_delegate.vala

+using Gtk;
+using Cairo;
+
+/** Separate class for status */
+public class StatusDelegate : EventBox {
+	
+	private Status? status = null;
+	private AStream? stream = null;
+	
+	private VBox vb_main;
+	private BgBox hb_main;
+	
+	private Avatar avatar;
+	private Label nick;
+	private Label date;
+	private WrapLabel content;
+	private ReplyLabel re_label;
+	private bool already_expanded = false;
+	private ConversationView? con_view = null;
+	private VBox vb_right;
+	
+	private SmartTimer timer;
+	
+	private Gdk.Pixbuf? rt_pixbuf = null;
+	private const double MAX_RGB = (double) uint16.MAX;
+	
+	private string date_string = "<small><span foreground='#888'><b>%s</b></span></small>";
+	
+	private Regex nicks;
+	private Regex tags;
+	private Regex groups;
+	private Regex urls;
+	private Regex clear_notice;
+	
+	public StatusDelegate(Status status, AStream stream) {
+		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;
+		this.stream = stream;
+		
+		set_events(Gdk.EventMask.BUTTON_RELEASE_MASK);
+		set_events(Gdk.EventMask.BUTTON_PRESS_MASK);
+		button_release_event.connect(on_click);
+		button_press_event.connect(on_double_click);
+		
+		vb_main = new VBox(false, 0);
+		
+		hb_main = new BgBox(false, 0);
+		hb_main.fresh = status.fresh;
+		hb_main.favorited = status.favorited;
+		
+		//update background if fresh status changed
+		status.notify["fresh"].connect((s, p) => {
+			hb_main.fresh = ((Status) s).fresh;
+		});
+		
+		status.notify["favorited"].connect((s, p) => {
+			hb_main.favorited = ((Status) s).favorited;
+		});
+		
+		VBox vb_avatar = new VBox(false, 0);
+		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(date_string.printf(time_to_human_delta(status.created)));
+		
+		timer = new SmartTimer(60);
+		timer.timeout.connect(update_date);
+		
+		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));
+		content.link_activated.connect(uri_route);
+		main_window.set_focus.connect(unfocused);
+		
+		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'>%s </span>%s</b></small>".printf(_("retweeted by"), 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);*/
+				
+				re_label = new ReplyLabel(status.reply.name);
+				re_label.set_tooltip(_("Show conversation"));
+				
+				re_label.clicked.connect(re_label_clicked);
+				
+				vb_right.pack_start(re_label, false, false, 4);
+				
+				status.new_reply.connect(add_new_reply);
+				
+				status.end_reply.connect(end_reply);
+				
+			} 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);
+		
+		vb_main.pack_start(hb_main, true, true, 0);
+		
+		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(vb_main);
+		
+		//set bg color
+		Gdk.Color color = Gdk.Color();
+		Gdk.Color.parse("white", out color);
+		
+		modify_bg(StateType.NORMAL, color);
+	}
+	
+	private void re_label_clicked() {
+		re_label.set_sensitive(false);
+		re_label.start();
+		
+		/*
+		if(already_expanded) {
+			if(con_view.visible) {
+				con_view.hide_all();
+				re_label.set_tooltip(tooltip_show);
+			} else {
+				con_view.show_all();
+				re_label.set_tooltip(tooltip_hide);
+			}
+			
+			return;
+		}*/
+		
+		stream.account.get_conversation(status);
+		
+		//already_expanded = true;
+	}
+	
+	private void add_new_reply(Status nstatus) { //new reply received
+		debug(nstatus.id);
+		
+		if(con_view == null) {
+			debug("ok");
+			con_view = new ConversationView();
+			vb_main.pack_start(con_view, false, false, 0);
+			con_view.show_all();
+		}
+		
+		con_view.add_delegate(new StatusDelegate(nstatus, stream));
+	}
+	
+	private void end_reply() {
+		re_label.stop();
+		re_label.set_tooltip("");
+	}
+	
+	private void update_date() {
+		date.set_markup(date_string.printf(time_to_human_delta(status.created)));
+	}
+	
+	/** Any click makes it not fresh */
+	private bool on_click(Gdk.EventButton event) {
+		switch(event.button) {
+		case 1: //left click
+			if(!status.fresh)
+				return true;
+			
+			status.fresh = false;
+			debug("ok");
+			return true;
+		
+		case 3: //context menu
+			stream.account.context_menu(stream, status);
+			return true;
+		
+		default:
+			return false;
+		}
+	}
+	
+	private bool on_double_click(Gdk.EventButton event) {
+		if(event.type != Gdk.EventType.2BUTTON_PRESS)
+			return false;
+		
+		debug("double click");
+		content.set_selectable(true);
+		main_window.set_focus(content);
+		
+		return true;
+	}
+	
+	private void unfocused(Widget? widget) {
+		if(widget == null || widget == content)
+			return;
+		
+		content.set_selectable(false);
+	}
+	
+	private void uri_route(string prot, string uri) {
+		switch(prot) {
+		case "search":
+			stream.account.go_hashtag(uri);
+			break;
+		
+		case "userinfo":
+			debug("not implemented");
+			break;
+		
+		default:
+			GLib.Pid pid;
+			GLib.Process.spawn_async(".", {"/usr/bin/xdg-open", prot + "://" + uri}, null,
+				GLib.SpawnFlags.STDOUT_TO_DEV_NULL, null, out pid);
+			break;
+		}
+	}
+	
+	/** 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) {
+			//timer.set_interval(120);
+			return _("1 minute ago");
+		}
+		if(delta < 3600) {
+			timer.set_interval(300);
+			return _("%i minutes ago").printf(delta / 60);
+		}
+		if(delta < 7200) {
+			timer.set_interval(3600);
+			return _("about 1 hour ago");
+		}
+		if(delta < 86400) {
+			timer.set_interval(3600);
+			return _("about %i hours ago").printf(delta / 3600);
+		}
+		
+		timer.set_interval(0);
+		
+		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;
+	}
+}

src/stream_abstract.vala

 	
 	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;}
 	
 	
 	public int fresh_items {get; set; default = 0;}
 	
+	construct {
+		model.stream = this;
+	}
+	
 	public virtual void menu_refresh() {
 		debug("not implemented");
 	}
+/* timer.vala
+ *
+ * Copyright (C) 2009-2010  troorl
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author:
+ * 	troorl <troorl@gmail.com>
+ */
+
+public class SmartTimer : Object {
+	
+	private double interval;
+	private double elapsed = 0;
+	
+	public signal void timeout();
+	
+	public SmartTimer(uint interval) {
+		this.interval = interval;
+		Timeout.add_seconds(60, callback);
+	}
+	
+	public void set_interval(double interval) {
+		this.interval = interval;
+		elapsed = 0;
+	}
+	
+	private bool callback() {
+		if(interval == 0)
+			return true;
+		
+		elapsed += 60;
+		
+		if(elapsed >= interval) {
+			elapsed = 0;
+			timeout();
+		}
+		
+		return true;
+	}
+}

src/tree_widget.vala

 
 public class TreeWidget : TreeView {
 	
-	public signal void cursor_moved(string hash);
+	public signal void cursor_moved(AStream stream);
 	
 	private Window parent;
 	private Accounts accounts;
 			
 			string hash = active_account.get_stream_hash(active_stream);
 
-			cursor_moved(hash);
+			cursor_moved(active_stream);
 		}
 	}
 
 	}
 	
 	/** 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);
 		

src/twitter_account.vala