twterm 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -245,17 +245,15 @@ module Twterm
245
245
  end
246
246
  end
247
247
 
248
- def post(text, in_reply_to = nil)
248
+ def post(text, options = {})
249
249
  send_request do
250
- if in_reply_to.is_a? Status
251
- user = user_repository.find(in_reply_to.user_id)
252
- text = "@#{user.screen_name} #{text}"
253
- rest_client.update(text, in_reply_to_status_id: in_reply_to.id)
254
- else
255
- rest_client.update(text)
256
- end
257
- publish(Event::Notification::Success.new('Your tweet has been posted'))
250
+ rest_client.update(text, options)
258
251
  end
252
+ .then do |status|
253
+ publish(Event::Notification::Success.new('Your tweet has been posted'))
254
+
255
+ status
256
+ end
259
257
  end
260
258
 
261
259
  def rate_limit_status
@@ -25,5 +25,6 @@ class Scheduler
25
25
 
26
26
  def run
27
27
  @block.call unless @paused
28
+ rescue
28
29
  end
29
30
  end
data/lib/twterm/status.rb CHANGED
@@ -1,10 +1,19 @@
1
1
  require 'concurrent'
2
2
 
3
+ class Twitter::Tweet
4
+ attr_reader :quoted_status_id
5
+
6
+ def initialize(*args)
7
+ @quoted_status_id = args[0][:quoted_status_id]
8
+ super
9
+ end
10
+ end
11
+
3
12
  module Twterm
4
13
  class Status
5
14
  attr_reader :created_at, :favorite_count, :favorited, :id,
6
15
  :in_reply_to_status_id, :media, :retweet_count, :retweeted,
7
- :retweeted_status_id, :text, :urls, :user_id
16
+ :quoted_status_id, :retweeted_status_id, :text, :url, :urls, :user_id
8
17
  alias_method :favorited?, :favorited
9
18
  alias_method :retweeted?, :retweeted
10
19
 
@@ -35,6 +44,8 @@ module Twterm
35
44
  @text = CGI.unescapeHTML(tweet.full_text.dup)
36
45
  @created_at = tweet.created_at.dup.localtime
37
46
  @in_reply_to_status_id = tweet.in_reply_to_status_id
47
+ @quoted_status_id = tweet.quoted_status_id
48
+ @url = tweet.url
38
49
 
39
50
  update!(tweet, is_retweeted_status)
40
51
 
@@ -48,6 +59,10 @@ module Twterm
48
59
  expand_url!
49
60
  end
50
61
 
62
+ def quote?
63
+ !quoted_status_id.nil?
64
+ end
65
+
51
66
  def retweet?
52
67
  !retweeted_status_id.nil?
53
68
  end
@@ -36,9 +36,9 @@ module Twterm
36
36
 
37
37
  input_thread = Thread.new do
38
38
  close_screen
39
- app.completion_manager.set_default_mode!
39
+ app.completion_manager.set_search_mode!
40
40
  puts "\ninput search query"
41
- query = (readline('> ') || '').strip
41
+ query = (readline('> ', true) || '').strip
42
42
  resetter.call
43
43
 
44
44
  tab = query.nil? || query.empty? ? Tab::New::Search.new(app, client) : Tab::Statuses::Search.new(app, client, query)
@@ -25,7 +25,7 @@ module Twterm
25
25
  input_thread = Thread.new do
26
26
  close_screen
27
27
  puts "\nSearch user"
28
- screen_name = (readline('> @') || '').strip
28
+ screen_name = (readline('> @', true) || '').strip
29
29
  resetter.call
30
30
 
31
31
  if screen_name.nil? || screen_name.empty?
@@ -116,10 +116,16 @@ module Twterm
116
116
  render
117
117
  end
118
118
 
119
+ def quote
120
+ return if highlighted_status.nil?
121
+
122
+ app.tweetbox.quote(highlighted_original_status)
123
+ end
124
+
119
125
  def reply
120
126
  return if highlighted_status.nil?
121
127
 
122
- app.tweetbox.compose(highlighted_original_status)
128
+ app.tweetbox.reply(highlighted_original_status)
123
129
  end
124
130
 
125
131
  def respond_to_key(key)
@@ -142,6 +148,8 @@ module Twterm
142
148
  retweet
143
149
  when k[:tab, :reload]
144
150
  reload
151
+ when k[:status, :quote]
152
+ quote
145
153
  when k[:status, :user]
146
154
  show_user
147
155
  else
@@ -0,0 +1,18 @@
1
+ module Twterm
2
+ module Tab
3
+ module Statuses
4
+ module Cacheable
5
+ def retrieve_from_cache!
6
+ statuses = cached_statuses
7
+ cached_statuses.each { |status| append(status) }
8
+
9
+ unless statuses.empty?
10
+ sort
11
+ scroller.move_to_top
12
+ initially_loaded!
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,11 +1,14 @@
1
1
  require 'concurrent'
2
2
 
3
+ require 'twterm/tab/dumpable'
3
4
  require 'twterm/tab/statuses/base'
5
+ require 'twterm/tab/statuses/cacheable'
4
6
 
5
7
  module Twterm
6
8
  module Tab
7
9
  module Statuses
8
10
  class Conversation < Base
11
+ include Cacheable
9
12
  include Dumpable
10
13
 
11
14
  attr_reader :status_id
@@ -18,7 +21,10 @@ module Twterm
18
21
  find_or_fetch_status(status_id).then do |status|
19
22
  append(status)
20
23
  fetch_ancestor(status)
24
+ fetch_quoted_status(status)
21
25
  find_descendants(status)
26
+ fetch_possible_quotes(status)
27
+ fetch_possible_replies(status)
22
28
  end
23
29
  end
24
30
 
@@ -40,11 +46,62 @@ module Twterm
40
46
  end
41
47
  end
42
48
 
49
+ def fetch_possible_quotes(status)
50
+ client.search(status.url).then do |statuses|
51
+ statuses
52
+ .select { |s| !s.retweet? && s.quoted_status_id == status.id }
53
+ .each { |s| prepend(s) }
54
+
55
+ sort
56
+ render
57
+ end
58
+ end
59
+
60
+ def fetch_possible_replies(status)
61
+ user = app.user_repository.find(status.user_id)
62
+
63
+ return if user.nil?
64
+
65
+ client.search("to:#{user.screen_name}").then do |statuses|
66
+ statuses
67
+ .select { |s| !s.retweet? && s.in_reply_to_status_id == status.id }
68
+ .each { |s| prepend(s) }
69
+
70
+ sort
71
+ render
72
+ end
73
+ end
74
+
75
+ def fetch_quoted_status(status)
76
+ quoted_status_id = status.quoted_status_id
77
+
78
+ if quoted_status_id.nil?
79
+ Concurrent::Promise.fulfill(nil)
80
+ elsif (instance = app.status_repository.find(quoted_status_id))
81
+ Concurrent::Promise.fulfill(instance)
82
+ else
83
+ client.show_status(quoted_status_id)
84
+ end
85
+ .then do |quoted_status|
86
+ next if quoted_status.nil?
87
+ append(quoted_status)
88
+ sort
89
+ fetch_ancestor(quoted_status)
90
+ fetch_quoted_status(quoted_status)
91
+ end
92
+ end
93
+
43
94
  def find_descendants(status)
44
- app.status_repository.find_replies_for(status.id).each do |reply|
95
+ app.status_repository.find_replies_for(status.id).reject { |s| s.retweet? }.each do |reply|
45
96
  prepend(reply)
46
97
  find_descendants(reply)
47
98
  end
99
+
100
+ app.status_repository.find_quotes_for(status.id).reject { |s| s.retweet? }.each do |quote|
101
+ prepend(quote)
102
+ find_descendants(quote)
103
+ end
104
+
48
105
  sort
49
106
  end
50
107
 
@@ -57,6 +114,8 @@ module Twterm
57
114
 
58
115
  @status_id = status_id
59
116
 
117
+ retrieve_from_cache!
118
+
60
119
  reload.then do
61
120
  scroller.move_to_top
62
121
  sort
@@ -66,6 +125,12 @@ module Twterm
66
125
  def title
67
126
  'Conversation'.freeze
68
127
  end
128
+
129
+ private
130
+
131
+ def cached_statuses
132
+ [app.status_repository.find(status_id)].compact
133
+ end
69
134
  end
70
135
  end
71
136
  end
@@ -15,6 +15,8 @@ module Twterm
15
15
 
16
16
  self.title = 'Loading...'.freeze
17
17
 
18
+ @auto_reloader = Scheduler.new(300) { reload }
19
+
18
20
  find_or_fetch_list(list_id).then do |list|
19
21
  self.title = list.full_name
20
22
  app.tab_manager.refresh_window
@@ -23,8 +25,6 @@ module Twterm
23
25
  initially_loaded!
24
26
  scroller.move_to_top
25
27
  end
26
-
27
- @auto_reloader = Scheduler.new(300) { reload }
28
28
  end
29
29
  end
30
30
 
@@ -1,9 +1,11 @@
1
1
  require 'twterm/tab/statuses/base'
2
+ require 'twterm/tab/statuses/cacheable'
2
3
 
3
4
  module Twterm
4
5
  module Tab
5
6
  module Statuses
6
7
  class UserTimeline < Base
8
+ include Cacheable
7
9
  include Dumpable
8
10
 
9
11
  attr_reader :user, :user_id
@@ -29,6 +31,9 @@ module Twterm
29
31
  super(app, client)
30
32
 
31
33
  @user_id = user_id
34
+ @auto_reloader = Scheduler.new(120) { reload }
35
+
36
+ retrieve_from_cache!
32
37
 
33
38
  find_or_fetch_user(user_id).then do |user|
34
39
  @user = user
@@ -38,14 +43,18 @@ module Twterm
38
43
  initially_loaded!
39
44
  scroller.move_to_top
40
45
  end
41
-
42
- @auto_reloader = Scheduler.new(120) { reload }
43
46
  end
44
47
  end
45
48
 
46
49
  def title
47
50
  @user.nil? ? 'Loading...' : "@#{@user.screen_name} timeline"
48
51
  end
52
+
53
+ private
54
+
55
+ def cached_statuses
56
+ app.status_repository.all.select { |status| status.user_id == user_id }
57
+ end
49
58
  end
50
59
  end
51
60
  end
@@ -15,109 +15,98 @@ module Twterm
15
15
  @app, @client = app, client
16
16
  end
17
17
 
18
- def compose(in_reply_to = nil)
19
- @text = ''
20
-
21
- @in_reply_to, screen_name =
22
- if in_reply_to.is_a?(Status)
23
- [in_reply_to, app.user_repository.find(in_reply_to.user_id).screen_name]
24
- else
25
- [nil, nil]
26
- end
18
+ def compose
19
+ ask_and_post("\e[1mCompose new Tweet\e[0m", '> ', -> body { body })
20
+ end
27
21
 
28
- resetter = proc do
29
- reset_prog_mode
30
- sleep 0.1
31
- app.screen.refresh
32
- end
22
+ def quote(status)
23
+ screen_name = app.user_repository.find(status.user_id).screen_name
24
+ leading_text = "\e[1mQuoting @#{screen_name}'s Tweet\e[0m\n\n#{status.text}"
25
+ prompt = '> '
33
26
 
34
- thread = Thread.new do
35
- close_screen
27
+ ask_and_post(leading_text, prompt, -> body { "#{body} #{status.url}" })
28
+ end
36
29
 
37
- if in_reply_to.nil?
38
- puts "\nCompose new tweet:"
39
- else
40
- puts "\nReply to @#{screen_name}'s tweet: \"#{in_reply_to.text}\""
41
- end
30
+ def reply(status)
31
+ screen_name = app.user_repository.find(status.user_id).screen_name
32
+ leading_text = "\e[1mReplying to @#{screen_name}\e[0m\n\n#{status.text}"
33
+ prompt = "> @#{screen_name} "
42
34
 
43
- app.completion_manager.set_default_mode!
35
+ ask_and_post(leading_text, prompt, -> body { "@#{screen_name} #{body}" }, { in_reply_to_status_id: status.id })
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :app, :client, :in_reply_to
41
+
42
+ def ask(prompt, postprocessor, &cont)
43
+ app.completion_manager.set_default_mode!
44
+
45
+ thread = Thread.new do
46
+ raw_text = ''
44
47
 
45
48
  loop do
46
49
  loop do
47
- msg = in_reply_to.nil? || !text.empty? ? '> ' : "> @#{screen_name} "
48
- line = (readline(msg, true) || '').strip
50
+ line = (readline(prompt, true) || '').strip
49
51
  break if line.empty?
50
52
 
51
53
  if line.end_with?('\\')
52
- @text << line.chop.rstrip + "\n"
54
+ raw_text << line.chop.rstrip + "\n"
53
55
  else
54
- @text << line
56
+ raw_text << line
55
57
  break
56
58
  end
57
59
  end
58
60
 
59
61
  puts "\n"
60
62
 
63
+ text = postprocessor.call(raw_text)
64
+
61
65
  begin
62
- validate_text!
66
+ validate!(text)
63
67
  break
64
68
  rescue EmptyTextError
65
69
  break
66
70
  rescue InvalidCharactersError
67
71
  puts 'Text contains invalid characters'
68
72
  rescue TextTooLongError
69
- puts "Text is too long (#{text_length} / 140 characters)"
73
+ puts "Text is too long (#{text_length(text)} / 140 characters)"
70
74
  end
71
75
 
72
76
  puts "\n"
73
- clear
77
+ raw_text = ''
74
78
  end
75
79
 
76
- resetter.call
77
- post
80
+ reset
81
+ cont.call(raw_text) unless raw_text.empty?
78
82
  end
79
83
 
80
84
  app.register_interruption_handler do
81
85
  thread.kill
82
- clear
83
86
  puts "\nCanceled"
84
- resetter.call
87
+ reset
85
88
  end
86
89
 
87
90
  thread.join
88
91
  end
89
92
 
90
- private
91
-
92
- attr_reader :app, :client, :in_reply_to
93
-
94
- def clear
95
- @text = ''
96
- @in_reply_to = nil
97
- end
98
-
99
- def post
100
- validate_text!
101
- client.post(text, in_reply_to)
102
- rescue EmptyTextError
103
- # do nothing
104
- rescue InvalidCharactersError
105
- publish(Event::Notification::Error.new('Text contains invalid characters'))
106
- rescue TextTooLongError
107
- publish(Event::Notification::Error.new("Text is too long (#{text_length} / 140 characters)"))
108
- ensure
109
- clear
93
+ def ask_and_post(leading_text, prompt, postprocessor, options = {})
94
+ close_screen
95
+ puts "\e[H\e[2J#{leading_text}\n\n"
96
+ ask(prompt, postprocessor) { |text| client.post(postprocessor.call(text), options) }
110
97
  end
111
98
 
112
- def text
113
- @text || ''
99
+ def reset
100
+ reset_prog_mode
101
+ sleep 0.1
102
+ app.screen.refresh
114
103
  end
115
104
 
116
- def text_length
105
+ def text_length(text)
117
106
  Twitter::Validation.tweet_length(text)
118
107
  end
119
108
 
120
- def validate_text!
109
+ def validate!(text)
121
110
  case Twitter::Validation.tweet_invalid?(text)
122
111
  when :empty
123
112
  fail EmptyTextError