Merge lp:~sergiusens/account-polld/facebook_tweaks into lp:~ubuntu-push-hackers/account-polld/trunk
- facebook_tweaks
- Merge into trunk
Proposed by
Sergio Schvezov
Status: | Merged |
---|---|
Approved by: | Sergio Schvezov |
Approved revision: | 59 |
Merged at revision: | 54 |
Proposed branch: | lp:~sergiusens/account-polld/facebook_tweaks |
Merge into: | lp:~ubuntu-push-hackers/account-polld/trunk |
Prerequisite: | lp:~sergiusens/account-polld/gmail_persist |
Diff against target: |
570 lines (+226/-105) 6 files modified
cmd/account-polld/main.go (+2/-2) plugins/facebook/facebook.go (+125/-27) plugins/facebook/facebook_test.go (+8/-5) plugins/gmail/gmail.go (+20/-61) plugins/plugins.go (+58/-4) po/account-polld.pot (+13/-6) |
To merge this branch: | bzr merge lp:~sergiusens/account-polld/facebook_tweaks |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Roberto Alsina (community) | Approve | ||
PS Jenkins bot | continuous-integration | Approve | |
Review via email: mp+230174@code.launchpad.net |
Commit message
Facebook pretty notifications, persistence (with multiple account support)
Description of the change
This is not just facebook tweaks, but also some changes to have multi account logging and persistance per plugin
To post a comment you must log in.
- 59. By Sergio Schvezov
-
Updated translation template
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : | # |
review:
Approve
(continuous-integration)
Revision history for this message
Roberto Alsina (ralsina) : | # |
review:
Approve
- 60. By Sergio Schvezov
-
Filter notifications for facebook at start, don't do a consolidated message if no usernames where collected log more for facebook for now
- 61. By Sergio Schvezov
-
Updated translation template
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'cmd/account-polld/main.go' |
2 | --- cmd/account-polld/main.go 2014-08-01 19:27:53 +0000 |
3 | +++ cmd/account-polld/main.go 2014-08-11 18:35:47 +0000 |
4 | @@ -94,11 +94,11 @@ |
5 | switch data.ServiceName { |
6 | case SERVICENAME_GMAIL: |
7 | log.Println("Creating account with id", data.AccountId, "for", data.ServiceName) |
8 | - plugin = gmail.New() |
9 | + plugin = gmail.New(data.AccountId) |
10 | case SERVICENAME_FACEBOOK: |
11 | // This is just stubbed until the plugin exists. |
12 | log.Println("Creating account with id", data.AccountId, "for", data.ServiceName) |
13 | - plugin = facebook.New() |
14 | + plugin = facebook.New(data.AccountId) |
15 | case SERVICENAME_TWITTER: |
16 | // This is just stubbed until the plugin exists. |
17 | log.Println("Creating account with id", data.AccountId, "for", data.ServiceName) |
18 | |
19 | === modified file 'plugins/facebook/facebook.go' |
20 | --- plugins/facebook/facebook.go 2014-08-01 20:30:06 +0000 |
21 | +++ plugins/facebook/facebook.go 2014-08-11 18:35:47 +0000 |
22 | @@ -1,6 +1,7 @@ |
23 | /* |
24 | Copyright 2014 Canonical Ltd. |
25 | Authors: James Henstridge <james.henstridge@canonical.com> |
26 | + Sergio Schvezov <sergio.schvezov@canonical.com> |
27 | |
28 | This program is free software: you can redistribute it and/or modify it |
29 | under the terms of the GNU General Public License version 3, as published |
30 | @@ -19,24 +20,63 @@ |
31 | |
32 | import ( |
33 | "encoding/json" |
34 | + "fmt" |
35 | "net/http" |
36 | "net/url" |
37 | + "os" |
38 | + "strings" |
39 | "time" |
40 | |
41 | + "log" |
42 | + |
43 | "launchpad.net/account-polld/accounts" |
44 | + "launchpad.net/account-polld/gettext" |
45 | "launchpad.net/account-polld/plugins" |
46 | ) |
47 | |
48 | -const facebookTime = "2006-01-02T15:04:05-0700" |
49 | +const ( |
50 | + facebookTime = "2006-01-02T15:04:05-0700" |
51 | + maxIndividualNotifications = 4 |
52 | + consolidatedNotificationsIndexStart = maxIndividualNotifications |
53 | + pluginName = "facebook" |
54 | +) |
55 | |
56 | var baseUrl, _ = url.Parse("https://graph.facebook.com/v2.0/") |
57 | |
58 | +type timeStamp string |
59 | + |
60 | +func (stamp timeStamp) persist(accountId uint) (err error) { |
61 | + err = plugins.Persist(pluginName, accountId, stamp) |
62 | + if err != nil { |
63 | + log.Print("facebook plugin", accountId, ": failed to save state: ", err) |
64 | + } |
65 | + return nil |
66 | +} |
67 | + |
68 | +func timeStampFromStorage(accountId uint) (stamp timeStamp, err error) { |
69 | + err = plugins.FromPersist(pluginName, accountId, &stamp) |
70 | + if err != nil { |
71 | + return stamp, err |
72 | + } |
73 | + if _, err := time.Parse(facebookTime, string(stamp)); err == nil { |
74 | + return stamp, err |
75 | + } |
76 | + return stamp, nil |
77 | +} |
78 | + |
79 | type fbPlugin struct { |
80 | - lastUpdate string |
81 | + lastUpdate timeStamp |
82 | + accountId uint |
83 | } |
84 | |
85 | -func New() plugins.Plugin { |
86 | - return &fbPlugin{} |
87 | +func New(accountId uint) plugins.Plugin { |
88 | + stamp, err := timeStampFromStorage(accountId) |
89 | + if err != nil { |
90 | + log.Print("facebook plugin ", accountId, ": cannot load previous state from storage: ", err) |
91 | + } else { |
92 | + log.Print("facebook plugin ", accountId, ": last state loaded from storage") |
93 | + } |
94 | + return &fbPlugin{lastUpdate: stamp, accountId: accountId} |
95 | } |
96 | |
97 | func (p *fbPlugin) ApplicationId() plugins.ApplicationId { |
98 | @@ -75,29 +115,75 @@ |
99 | // page full of notifications. The default limit seems to be |
100 | // 5000 though, which we are unlikely to hit, since |
101 | // notifications are deleted once read. |
102 | + // TODO filter out of date messages before operating |
103 | var result notificationDoc |
104 | if err := decoder.Decode(&result); err != nil { |
105 | return nil, err |
106 | } |
107 | - pushMsg := []plugins.PushMessage{} |
108 | - latestUpdate := "" |
109 | + |
110 | + var validNotifications []notification |
111 | + latestUpdate := p.lastUpdate |
112 | for _, n := range result.Data { |
113 | if n.UpdatedTime <= p.lastUpdate { |
114 | - continue |
115 | + log.Println("facebook plugin: skipping notification", n.Id, "as", n.UpdatedTime, "is older than", p.lastUpdate) |
116 | + } else if n.Unread != 1 { |
117 | + log.Println("facebook plugin: skipping notification", n.Id, "as it's read:", n.Unread) |
118 | + } else { |
119 | + log.Println("facebook plugin: valid notification", n.Id, "dated:", n.UpdatedTime, "and read status:", n.Unread) |
120 | + validNotifications = append(validNotifications, n) |
121 | + if n.UpdatedTime > latestUpdate { |
122 | + latestUpdate = n.UpdatedTime |
123 | + } |
124 | } |
125 | - // TODO proper action needed |
126 | - action := "https://m.facebook.com" |
127 | + } |
128 | + p.lastUpdate = latestUpdate |
129 | + p.lastUpdate.persist(p.accountId) |
130 | + |
131 | + pushMsg := []plugins.PushMessage{} |
132 | + for _, n := range validNotifications { |
133 | epoch := toEpoch(n.UpdatedTime) |
134 | - pushMsg = append(pushMsg, *plugins.NewStandardPushMessage(n.Title, "", action, "", epoch)) |
135 | - if n.UpdatedTime > latestUpdate { |
136 | - latestUpdate = n.UpdatedTime |
137 | - } |
138 | - } |
139 | - p.lastUpdate = latestUpdate |
140 | + pushMsg = append(pushMsg, *plugins.NewStandardPushMessage(n.From.Name, n.Title, n.Link, n.picture(), epoch)) |
141 | + if len(pushMsg) == maxIndividualNotifications { |
142 | + break |
143 | + } |
144 | + } |
145 | + |
146 | + // Now we consolidate the remaining statuses |
147 | + if len(validNotifications) > len(pushMsg) && len(validNotifications) >= consolidatedNotificationsIndexStart { |
148 | + usernamesMap := make(map[string]bool) |
149 | + for _, n := range result.Data[consolidatedNotificationsIndexStart:] { |
150 | + if _, ok := usernamesMap[n.From.Name]; !ok { |
151 | + usernamesMap[n.From.Name] = true |
152 | + } |
153 | + } |
154 | + usernames := []string{} |
155 | + for k, _ := range usernamesMap { |
156 | + usernames = append(usernames, k) |
157 | + // we don't too many usernames listed, this is a hard number |
158 | + if len(usernames) > 10 { |
159 | + usernames = append(usernames, "...") |
160 | + break |
161 | + } |
162 | + } |
163 | + if len(usernames) > 0 { |
164 | + // TRANSLATORS: This represents a notification summary about more facebook notifications |
165 | + summary := gettext.Gettext("Multiple more notifications") |
166 | + // TRANSLATORS: This represents a notification body with the comma separated facebook usernames |
167 | + body := fmt.Sprintf(gettext.Gettext("From %s"), strings.Join(usernames, ", ")) |
168 | + action := "https://m.facebook.com" |
169 | + epoch := time.Now().Unix() |
170 | + pushMsg = append(pushMsg, *plugins.NewStandardPushMessage(summary, body, action, "", epoch)) |
171 | + } |
172 | + } |
173 | + |
174 | return pushMsg, nil |
175 | } |
176 | |
177 | func (p *fbPlugin) Poll(authData *accounts.AuthData) ([]plugins.PushMessage, error) { |
178 | + // This envvar check is to ease testing. |
179 | + if token := os.Getenv("ACCOUNT_POLLD_TOKEN_FACEBOOK"); token != "" { |
180 | + authData.AccessToken = token |
181 | + } |
182 | resp, err := p.request(authData, "me/notifications") |
183 | if err != nil { |
184 | return nil, err |
185 | @@ -105,8 +191,8 @@ |
186 | return p.parseResponse(resp) |
187 | } |
188 | |
189 | -func toEpoch(timestamp string) int64 { |
190 | - if t, err := time.Parse(facebookTime, timestamp); err == nil { |
191 | +func toEpoch(stamp timeStamp) int64 { |
192 | + if t, err := time.Parse(facebookTime, string(stamp)); err == nil { |
193 | return t.Unix() |
194 | } |
195 | return time.Now().Unix() |
196 | @@ -123,16 +209,28 @@ |
197 | } |
198 | |
199 | type notification struct { |
200 | - Id string `json:"id"` |
201 | - From object `json:"from"` |
202 | - To object `json:"to"` |
203 | - CreatedTime string `json:"created_time"` |
204 | - UpdatedTime string `json:"updated_time"` |
205 | - Title string `json:"title"` |
206 | - Link string `json:"link"` |
207 | - Application object `json:"application"` |
208 | - Unread int `json:"unread"` |
209 | - Object object `json:"object"` |
210 | + Id string `json:"id"` |
211 | + From object `json:"from"` |
212 | + To object `json:"to"` |
213 | + CreatedTime timeStamp `json:"created_time"` |
214 | + UpdatedTime timeStamp `json:"updated_time"` |
215 | + Title string `json:"title"` |
216 | + Link string `json:"link"` |
217 | + Application object `json:"application"` |
218 | + Unread int `json:"unread"` |
219 | + Object object `json:"object"` |
220 | +} |
221 | + |
222 | +func (n notification) picture() string { |
223 | + u, err := baseUrl.Parse(fmt.Sprintf("%s/picture", n.From.Id)) |
224 | + if err != nil { |
225 | + log.Println("facebook plugin: cannot get picture for", n.Id) |
226 | + return "" |
227 | + } |
228 | + query := u.Query() |
229 | + query.Add("redirect", "true") |
230 | + u.RawQuery = query.Encode() |
231 | + return u.String() |
232 | } |
233 | |
234 | type object struct { |
235 | |
236 | === modified file 'plugins/facebook/facebook_test.go' |
237 | --- plugins/facebook/facebook_test.go 2014-07-23 10:45:24 +0000 |
238 | +++ plugins/facebook/facebook_test.go 2014-08-11 18:35:47 +0000 |
239 | @@ -133,9 +133,11 @@ |
240 | messages, err := p.parseResponse(resp) |
241 | c.Assert(err, IsNil) |
242 | c.Assert(len(messages), Equals, 2) |
243 | - c.Check(messages[0].Notification.Card.Summary, Equals, "Sender posted on your timeline: \"The message...\"") |
244 | - c.Check(messages[1].Notification.Card.Summary, Equals, "Sender2's birthday was on July 7.") |
245 | - c.Check(p.lastUpdate, Equals, "2014-07-12T09:51:57+0000") |
246 | + c.Check(messages[0].Notification.Card.Summary, Equals, "Sender") |
247 | + c.Check(messages[0].Notification.Card.Body, Equals, "Sender posted on your timeline: \"The message...\"") |
248 | + c.Check(messages[1].Notification.Card.Summary, Equals, "Sender2") |
249 | + c.Check(messages[1].Notification.Card.Body, Equals, "Sender2's birthday was on July 7.") |
250 | + c.Check(p.lastUpdate, Equals, timeStamp("2014-07-12T09:51:57+0000")) |
251 | } |
252 | |
253 | func (s S) TestIgnoreOldNotifications(c *C) { |
254 | @@ -147,8 +149,9 @@ |
255 | messages, err := p.parseResponse(resp) |
256 | c.Assert(err, IsNil) |
257 | c.Assert(len(messages), Equals, 1) |
258 | - c.Check(messages[0].Notification.Card.Summary, Equals, "Sender posted on your timeline: \"The message...\"") |
259 | - c.Check(p.lastUpdate, Equals, "2014-07-12T09:51:57+0000") |
260 | + c.Check(messages[0].Notification.Card.Summary, Equals, "Sender") |
261 | + c.Check(messages[0].Notification.Card.Body, Equals, "Sender posted on your timeline: \"The message...\"") |
262 | + c.Check(p.lastUpdate, Equals, timeStamp("2014-07-12T09:51:57+0000")) |
263 | } |
264 | |
265 | func (s S) TestErrorResponse(c *C) { |
266 | |
267 | === modified file 'plugins/gmail/gmail.go' |
268 | --- plugins/gmail/gmail.go 2014-08-11 18:35:47 +0000 |
269 | +++ plugins/gmail/gmail.go 2014-08-11 18:35:47 +0000 |
270 | @@ -18,14 +18,12 @@ |
271 | package gmail |
272 | |
273 | import ( |
274 | - "bufio" |
275 | "encoding/json" |
276 | "fmt" |
277 | "net/http" |
278 | "net/mail" |
279 | "net/url" |
280 | "os" |
281 | - "path/filepath" |
282 | "sort" |
283 | "time" |
284 | |
285 | @@ -41,6 +39,7 @@ |
286 | gmailDispatchUrl = "https://mail.google.com/mail/mu/mp/#cv/priority/^smartlabel_%s/%s" |
287 | // this means 3 individual messages + 1 bundled notification. |
288 | individualNotificationsLimit = 2 |
289 | + pluginName = "gmail" |
290 | ) |
291 | |
292 | type reportedIdMap map[string]time.Time |
293 | @@ -53,87 +52,47 @@ |
294 | // trackDelta defines how old messages can be before removed from tracking |
295 | var trackDelta = time.Duration(time.Hour * 24 * 7) |
296 | |
297 | -var idStoreSubPath = filepath.Join("gmail", "reportedIds.json") |
298 | - |
299 | type GmailPlugin struct { |
300 | // reportedIds holds the messages that have already been notified. This |
301 | // approach is taken against timestamps as it avoids needing to call |
302 | // get on the message. |
303 | reportedIds reportedIdMap |
304 | + accountId uint |
305 | } |
306 | |
307 | -func idsFromStorage() (ids reportedIdMap, err error) { |
308 | - var p string |
309 | - defer func() { |
310 | - if err != nil { |
311 | - if p != "" { |
312 | - os.Remove(p) |
313 | - } |
314 | - } |
315 | - }() |
316 | - p, err = plugins.DataFind(idStoreSubPath) |
317 | - if err != nil { |
318 | - return nil, err |
319 | - } |
320 | - file, err := os.Open(p) |
321 | - if err != nil { |
322 | - return nil, err |
323 | - } |
324 | - defer file.Close() |
325 | - jsonReader := json.NewDecoder(file) |
326 | - if err := jsonReader.Decode(&ids); err != nil { |
327 | - return nil, err |
328 | - } |
329 | - |
330 | +func idsFromPersist(accountId uint) (ids reportedIdMap, err error) { |
331 | + err = plugins.FromPersist(pluginName, accountId, &ids) |
332 | + if err != nil { |
333 | + return nil, err |
334 | + } |
335 | // discard old ids |
336 | timestamp := time.Now() |
337 | for k, v := range ids { |
338 | delta := timestamp.Sub(v) |
339 | if delta > trackDelta { |
340 | - log.Println("gmail plugin: deleting", k, "as", delta, "is greater than", trackDelta) |
341 | + log.Print("gmail plugin ", accountId, ": deleting ", k, " as ", delta, " is greater than ", trackDelta) |
342 | delete(ids, k) |
343 | } |
344 | } |
345 | return ids, nil |
346 | } |
347 | |
348 | -func (ids reportedIdMap) persist() (err error) { |
349 | - var p string |
350 | - defer func() { |
351 | - if err != nil { |
352 | - log.Println("gmail plugin: failed to save state:", err) |
353 | - if p != "" { |
354 | - os.Remove(p) |
355 | - } |
356 | - } |
357 | - }() |
358 | - p, err = plugins.DataEnsure(idStoreSubPath) |
359 | - if err != nil { |
360 | - return err |
361 | - } |
362 | - file, err := os.Create(p) |
363 | - if err != nil { |
364 | - return err |
365 | - } |
366 | - defer file.Close() |
367 | - w := bufio.NewWriter(file) |
368 | - defer w.Flush() |
369 | - jsonWriter := json.NewEncoder(w) |
370 | - if err := jsonWriter.Encode(ids); err != nil { |
371 | - return err |
372 | - } |
373 | - |
374 | +func (ids reportedIdMap) persist(accountId uint) (err error) { |
375 | + err = plugins.Persist(pluginName, accountId, ids) |
376 | + if err != nil { |
377 | + log.Print("gmail plugin ", accountId, ": failed to save state: ", err) |
378 | + } |
379 | return nil |
380 | } |
381 | |
382 | -func New() *GmailPlugin { |
383 | - reportedIds, err := idsFromStorage() |
384 | +func New(accountId uint) *GmailPlugin { |
385 | + reportedIds, err := idsFromPersist(accountId) |
386 | if err != nil { |
387 | - log.Println("gmail plugin: cannot load previous state from storage:", err) |
388 | + log.Print("gmail plugin ", accountId, ": cannot load previous state from storage: ", err) |
389 | } else { |
390 | - log.Println("gmail plugin: report state loaded from storage") |
391 | + log.Print("gmail plugin ", accountId, ": last state loaded from storage") |
392 | } |
393 | - return &GmailPlugin{reportedIds} |
394 | + return &GmailPlugin{reportedIds: reportedIds, accountId: accountId} |
395 | } |
396 | |
397 | func (p *GmailPlugin) ApplicationId() plugins.ApplicationId { |
398 | @@ -202,7 +161,7 @@ |
399 | epoch := hdr.getEpoch() |
400 | pushMsgMap[msg.ThreadId] = *plugins.NewStandardPushMessage(summary, body, action, "", epoch) |
401 | } else { |
402 | - log.Println("gmail: skipping message id", msg.Id, "with date", msgStamp, "older than", timeDelta) |
403 | + log.Print("gmail plugin ", p.accountId, ": skipping message id ", msg.Id, " with date ", msgStamp, " older than ", timeDelta) |
404 | } |
405 | } |
406 | var pushMsg []plugins.PushMessage |
407 | @@ -268,7 +227,7 @@ |
408 | ids[msg.Id] = time.Now() |
409 | } |
410 | p.reportedIds = ids |
411 | - p.reportedIds.persist() |
412 | + p.reportedIds.persist(p.accountId) |
413 | return reportMsg |
414 | } |
415 | |
416 | |
417 | === modified file 'plugins/plugins.go' |
418 | --- plugins/plugins.go 2014-08-11 18:35:47 +0000 |
419 | +++ plugins/plugins.go 2014-08-11 18:35:47 +0000 |
420 | @@ -18,9 +18,13 @@ |
421 | package plugins |
422 | |
423 | import ( |
424 | + "bufio" |
425 | + "encoding/json" |
426 | "errors" |
427 | + "fmt" |
428 | "os" |
429 | "path/filepath" |
430 | + "reflect" |
431 | |
432 | "launchpad.net/account-polld/accounts" |
433 | "launchpad.net/go-xdg/v0" |
434 | @@ -166,12 +170,62 @@ |
435 | |
436 | var cmdName string |
437 | |
438 | -func DataFind(path string) (string, error) { |
439 | - return xdg.Data.Find(filepath.Join(cmdName, path)) |
440 | +// Persist stores the plugins data in a common location to a json file |
441 | +// from which it can recover later |
442 | +func Persist(pluginName string, accountId uint, data interface{}) (err error) { |
443 | + var p string |
444 | + defer func() { |
445 | + if err != nil && p != "" { |
446 | + os.Remove(p) |
447 | + } |
448 | + }() |
449 | + p, err = xdg.Data.Ensure(filepath.Join(cmdName, fmt.Sprintf("%s-%d.json", pluginName, accountId))) |
450 | + if err != nil { |
451 | + return err |
452 | + } |
453 | + file, err := os.Create(p) |
454 | + if err != nil { |
455 | + return err |
456 | + } |
457 | + defer file.Close() |
458 | + w := bufio.NewWriter(file) |
459 | + defer w.Flush() |
460 | + jsonWriter := json.NewEncoder(w) |
461 | + if err := jsonWriter.Encode(data); err != nil { |
462 | + return err |
463 | + } |
464 | + return nil |
465 | } |
466 | |
467 | -func DataEnsure(path string) (string, error) { |
468 | - return xdg.Data.Ensure(filepath.Join(cmdName, path)) |
469 | +// FromPersist restores the plugins data from a common location which |
470 | +// was stored in a json file |
471 | +func FromPersist(pluginName string, accountId uint, data interface{}) (err error) { |
472 | + if reflect.ValueOf(data).Kind() != reflect.Ptr { |
473 | + return errors.New("decode target is not a pointer") |
474 | + } |
475 | + var p string |
476 | + defer func() { |
477 | + if err != nil { |
478 | + if p != "" { |
479 | + os.Remove(p) |
480 | + } |
481 | + } |
482 | + }() |
483 | + p, err = xdg.Data.Find(filepath.Join(cmdName, fmt.Sprintf("%s-%d.json", pluginName, accountId))) |
484 | + if err != nil { |
485 | + return err |
486 | + } |
487 | + file, err := os.Open(p) |
488 | + if err != nil { |
489 | + return err |
490 | + } |
491 | + defer file.Close() |
492 | + jsonReader := json.NewDecoder(file) |
493 | + if err := jsonReader.Decode(&data); err != nil { |
494 | + return err |
495 | + } |
496 | + |
497 | + return nil |
498 | } |
499 | |
500 | // DefaultSound returns the path to the default sound for a Notification |
501 | |
502 | === modified file 'po/account-polld.pot' |
503 | --- po/account-polld.pot 2014-08-06 13:04:18 +0000 |
504 | +++ po/account-polld.pot 2014-08-11 18:35:47 +0000 |
505 | @@ -8,7 +8,7 @@ |
506 | msgstr "" |
507 | "Project-Id-Version: account-polld\n" |
508 | "Report-Msgid-Bugs-To: \n" |
509 | -"POT-Creation-Date: 2014-08-06 10:04-0300\n" |
510 | +"POT-Creation-Date: 2014-08-11 15:35-0300\n" |
511 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" |
512 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" |
513 | "Language-Team: LANGUAGE <LL@li.org>\n" |
514 | @@ -29,7 +29,9 @@ |
515 | msgstr "" |
516 | |
517 | #. TRANSLATORS: This represents a notification body with the comma separated twitter usernames |
518 | +#. TRANSLATORS: This represents a notification body with the comma separated facebook usernames |
519 | #: plugins/twitter/twitter.go:130 plugins/twitter/twitter.go:187 |
520 | +#: plugins/facebook/facebook.go:172 |
521 | #, c-format |
522 | msgid "From %s" |
523 | msgstr "" |
524 | @@ -40,19 +42,19 @@ |
525 | msgstr "" |
526 | |
527 | #. TRANSLATORS: the %s is an appended "from" corresponding to an specific email thread |
528 | -#: plugins/gmail/gmail.go:116 |
529 | +#: plugins/gmail/gmail.go:153 |
530 | #, c-format |
531 | msgid ", %s" |
532 | msgstr "" |
533 | |
534 | #. TRANSLATORS: the %s is the "from" header corresponding to a specific email |
535 | -#: plugins/gmail/gmail.go:119 |
536 | +#: plugins/gmail/gmail.go:156 |
537 | #, c-format |
538 | msgid "%s" |
539 | msgstr "" |
540 | |
541 | #. TRANSLATORS: the first %s refers to the email "subject", the second %s refers "from" |
542 | -#: plugins/gmail/gmail.go:121 |
543 | +#: plugins/gmail/gmail.go:158 |
544 | #, c-format |
545 | msgid "" |
546 | "%s\n" |
547 | @@ -60,16 +62,21 @@ |
548 | msgstr "" |
549 | |
550 | #. TRANSLATORS: This represents a notification summary about more unread emails |
551 | -#: plugins/gmail/gmail.go:139 |
552 | +#: plugins/gmail/gmail.go:176 |
553 | msgid "More unread emails available" |
554 | msgstr "" |
555 | |
556 | #. TRANSLATORS: the first %d refers to approximate additionl email message count |
557 | -#: plugins/gmail/gmail.go:143 |
558 | +#: plugins/gmail/gmail.go:180 |
559 | #, c-format |
560 | msgid "You have an approximate of %d additional unread messages" |
561 | msgstr "" |
562 | |
563 | +#. TRANSLATORS: This represents a notification summary about more facebook notifications |
564 | +#: plugins/facebook/facebook.go:170 |
565 | +msgid "Multiple more notifications" |
566 | +msgstr "" |
567 | + |
568 | #: data/account-polld.desktop.tr.h:1 |
569 | msgid "Notifications" |
570 | msgstr "" |
PASSED: Continuous integration, rev:59 jenkins. qa.ubuntu. com/job/ account- polld-ci/ 62/ jenkins. qa.ubuntu. com/job/ account- polld-utopic- amd64-ci/ 62 jenkins. qa.ubuntu. com/job/ account- polld-utopic- armhf-ci/ 62 jenkins. qa.ubuntu. com/job/ account- polld-utopic- armhf-ci/ 62/artifact/ work/output/ *zip*/output. zip jenkins. qa.ubuntu. com/job/ account- polld-utopic- i386-ci/ 62
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/account- polld-ci/ 62/rebuild
http://