OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2009 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #include "chrome/browser/sync/notifier/communicator/mailbox.h" |
| 6 |
| 7 #include <assert.h> |
| 8 #include <stdlib.h> |
| 9 |
| 10 #include <stack> |
| 11 #include <vector> |
| 12 |
| 13 #include "chrome/browser/sync/notifier/base/string.h" |
| 14 #include "chrome/browser/sync/notifier/base/utils.h" |
| 15 #include "chrome/browser/sync/notifier/communicator/xml_parse_helpers.h" |
| 16 #include "talk/base/basictypes.h" |
| 17 #include "talk/base/common.h" |
| 18 #include "talk/base/stringutils.h" |
| 19 #include "talk/xmllite/xmlelement.h" |
| 20 #include "talk/xmpp/constants.h" |
| 21 |
| 22 namespace notifier { |
| 23 |
| 24 // Labels are a list of strings seperated by a '|' character. |
| 25 // The '|' character is escaped with a backslash ('\\') and the |
| 26 // backslash is also escaped with a backslash. |
| 27 static void ParseLabelSet(const std::string& text, |
| 28 MessageThread::StringSet* labels) { |
| 29 const char* input_cur = text.c_str(); |
| 30 const char* input_end = input_cur + text.size(); |
| 31 char* result = new char[text.size() + 1]; |
| 32 char* next_write = result; |
| 33 |
| 34 while(input_cur < input_end) { |
| 35 if (*input_cur == '|') { |
| 36 if (next_write != result) { |
| 37 *next_write = '\0'; |
| 38 labels->insert(std::string(result)); |
| 39 next_write = result; |
| 40 } |
| 41 input_cur++; |
| 42 continue; |
| 43 } |
| 44 |
| 45 if (*input_cur == '\\') { |
| 46 // skip a character in the input and break if we are at the end |
| 47 input_cur++; |
| 48 if (input_cur >= input_end) |
| 49 break; |
| 50 } |
| 51 *next_write = *input_cur; |
| 52 next_write++; |
| 53 input_cur++; |
| 54 } |
| 55 |
| 56 if (next_write != result) { |
| 57 *next_write = '\0'; |
| 58 labels->insert(std::string(result)); |
| 59 } |
| 60 |
| 61 delete [] result; |
| 62 } |
| 63 |
| 64 // ----------------------------------------------------------------------------- |
| 65 |
| 66 std::string MailAddress::safe_name() const { |
| 67 if (!name().empty()) { |
| 68 return name(); |
| 69 } |
| 70 |
| 71 if (!address().empty()) { |
| 72 size_t at = address().find('@'); |
| 73 if (at == std::string::npos) { |
| 74 return address(); |
| 75 } |
| 76 |
| 77 if (at != 0) { |
| 78 return address().substr(0, at); |
| 79 } |
| 80 } |
| 81 |
| 82 return std::string("(unknown)"); |
| 83 } |
| 84 |
| 85 // ----------------------------------------------------------------------------- |
| 86 MessageThread::~MessageThread() { |
| 87 Clear(); |
| 88 } |
| 89 |
| 90 void MessageThread::Clear() { |
| 91 delete labels_; |
| 92 labels_ = NULL; |
| 93 |
| 94 delete senders_; |
| 95 senders_ = NULL; |
| 96 } |
| 97 |
| 98 MessageThread& MessageThread::operator=(const MessageThread& r) { |
| 99 if (&r != this) { |
| 100 Clear(); |
| 101 // Copy everything |
| 102 r.AssertValid(); |
| 103 thread_id_ = r.thread_id_; |
| 104 date64_ = r.date64_; |
| 105 message_count_ = r.message_count_; |
| 106 personal_level_ = r.personal_level_; |
| 107 subject_ = r.subject_; |
| 108 snippet_ = r.snippet_; |
| 109 |
| 110 if (r.labels_) |
| 111 labels_ = new StringSet(*r.labels_); |
| 112 else |
| 113 labels_ = new StringSet; |
| 114 if (r.senders_) |
| 115 senders_ = new MailSenderList(*r.senders_); |
| 116 else |
| 117 senders_ = new MailSenderList; |
| 118 } |
| 119 AssertValid(); |
| 120 return *this; |
| 121 } |
| 122 |
| 123 MessageThread* MessageThread::CreateFromXML( |
| 124 const buzz::XmlElement* src) { |
| 125 MessageThread* info = new MessageThread(); |
| 126 if (!info || !info->InitFromXml(src)) { |
| 127 delete info; |
| 128 return NULL; |
| 129 } |
| 130 return info; |
| 131 } |
| 132 |
| 133 // Init from a chunk of XML |
| 134 bool MessageThread::InitFromXml(const buzz::XmlElement* src) { |
| 135 labels_ = new StringSet; |
| 136 senders_ = new MailSenderList; |
| 137 |
| 138 if (src->Name() != buzz::kQnMailThreadInfo) |
| 139 return false; |
| 140 |
| 141 if (!ParseInt64Attr(src, buzz::kQnMailTid, &thread_id_)) |
| 142 return false; |
| 143 if (!ParseInt64Attr(src, buzz::kQnMailDate, &date64_)) |
| 144 return false; |
| 145 if (!ParseIntAttr(src, buzz::kQnMailMessages, &message_count_)) |
| 146 return false; |
| 147 if (!ParseIntAttr(src, buzz::kQnMailParticipation, &personal_level_)) |
| 148 return false; |
| 149 |
| 150 const buzz::XmlElement* senders = src->FirstNamed(buzz::kQnMailSenders); |
| 151 if (!senders) |
| 152 return false; |
| 153 for (const buzz::XmlElement* child = senders->FirstElement(); |
| 154 child != NULL; |
| 155 child = child->NextElement()) { |
| 156 if (child->Name() != buzz::kQnMailSender) |
| 157 continue; |
| 158 std::string address; |
| 159 if (!ParseStringAttr(child, buzz::kQnMailAddress, &address)) |
| 160 continue; |
| 161 std::string name; |
| 162 ParseStringAttr(child, buzz::kQnMailName, &name); |
| 163 bool originator = false; |
| 164 ParseBoolAttr(child, buzz::kQnMailOriginator, &originator); |
| 165 bool unread = false; |
| 166 ParseBoolAttr(child, buzz::kQnMailUnread, &unread); |
| 167 |
| 168 senders_->push_back(MailSender(name, address, unread, originator)); |
| 169 } |
| 170 |
| 171 const buzz::XmlElement* labels = src->FirstNamed(buzz::kQnMailLabels); |
| 172 if (!labels) |
| 173 return false; |
| 174 ParseLabelSet(labels->BodyText(), labels_); |
| 175 |
| 176 const buzz::XmlElement* subject = src->FirstNamed(buzz::kQnMailSubject); |
| 177 if (!subject) |
| 178 return false; |
| 179 subject_ = subject->BodyText(); |
| 180 |
| 181 const buzz::XmlElement* snippet = src->FirstNamed(buzz::kQnMailSnippet); |
| 182 if (!snippet) |
| 183 return false; |
| 184 snippet_ = snippet->BodyText(); |
| 185 |
| 186 AssertValid(); |
| 187 return true; |
| 188 } |
| 189 |
| 190 bool MessageThread::starred() const { |
| 191 return (labels_->find("^t") != labels_->end()); |
| 192 } |
| 193 |
| 194 bool MessageThread::unread() const { |
| 195 return (labels_->find("^u") != labels_->end()); |
| 196 } |
| 197 |
| 198 #ifdef _DEBUG |
| 199 // non-debug version is inline and empty |
| 200 void MessageThread::AssertValid() const { |
| 201 assert(thread_id_ != 0); |
| 202 assert(senders_ != NULL); |
| 203 // In some (odd) cases, gmail may send email with no sender. |
| 204 // assert(!senders_->empty()); |
| 205 assert(message_count_ > 0); |
| 206 assert(labels_ != NULL); |
| 207 } |
| 208 #endif |
| 209 |
| 210 |
| 211 |
| 212 MailBox* MailBox::CreateFromXML(const buzz::XmlElement* src) { |
| 213 MailBox* mail_box = new MailBox(); |
| 214 if (!mail_box || !mail_box->InitFromXml(src)) { |
| 215 delete mail_box; |
| 216 return NULL; |
| 217 } |
| 218 return mail_box; |
| 219 } |
| 220 |
| 221 // Init from a chunk of XML |
| 222 bool MailBox::InitFromXml(const buzz::XmlElement* src) |
| 223 { |
| 224 if (src->Name() != buzz::kQnMailBox) |
| 225 return false; |
| 226 |
| 227 if (!ParseIntAttr(src, buzz::kQnMailTotalMatched, &mailbox_size_)) |
| 228 return false; |
| 229 |
| 230 estimate_ = false; |
| 231 ParseBoolAttr(src, buzz::kQnMailTotalEstimate, &estimate_); |
| 232 |
| 233 first_index_ = 0; |
| 234 ParseIntAttr(src, buzz::kQnMailFirstIndex, &first_index_); |
| 235 |
| 236 result_time_ = 0; |
| 237 ParseInt64Attr(src, buzz::kQnMailResultTime, &result_time_); |
| 238 |
| 239 highest_thread_id_ = 0; |
| 240 |
| 241 const buzz::XmlElement* thread_element = src->FirstNamed(buzz::kQnMailThreadIn
fo); |
| 242 while (thread_element) { |
| 243 MessageThread* thread = MessageThread::CreateFromXML(thread_element); |
| 244 if (thread) { |
| 245 if (thread->thread_id() > highest_thread_id_) |
| 246 highest_thread_id_ = thread->thread_id(); |
| 247 threads_.push_back(MessageThreadPointer(thread)); |
| 248 } |
| 249 thread_element = thread_element->NextNamed(buzz::kQnMailThreadInfo); |
| 250 } |
| 251 return true; |
| 252 } |
| 253 |
| 254 const size_t kMaxShortnameLength = 12; |
| 255 |
| 256 // Tip: If you extend this list of chars, do not include '-' |
| 257 const char name_delim[] = " ,.:;\'\"()[]{}<>*@"; |
| 258 |
| 259 class SenderFormatter { |
| 260 public: |
| 261 // sender should not be deleted while this class is in use. |
| 262 SenderFormatter(const MailSender& sender, |
| 263 const std::string& me_address) |
| 264 : sender_(sender), |
| 265 visible_(false), |
| 266 short_format_(true), |
| 267 space_(kMaxShortnameLength) { |
| 268 me_ = talk_base::ascicmp(me_address.c_str(), |
| 269 sender.address().c_str()) == 0; |
| 270 } |
| 271 |
| 272 bool visible() const { |
| 273 return visible_; |
| 274 } |
| 275 |
| 276 bool is_unread() const { |
| 277 return sender_.unread(); |
| 278 } |
| 279 |
| 280 const std::string name() const { |
| 281 return name_; |
| 282 } |
| 283 |
| 284 void set_short_format(bool short_format) { |
| 285 short_format_ = short_format; |
| 286 UpdateName(); |
| 287 } |
| 288 |
| 289 void set_visible(bool visible) { |
| 290 visible_ = visible; |
| 291 UpdateName(); |
| 292 } |
| 293 |
| 294 void set_space(size_t space) { |
| 295 space_ = space; |
| 296 UpdateName(); |
| 297 } |
| 298 |
| 299 private: |
| 300 // Attempt to shorten to the first word in a person's name |
| 301 // We could revisit and do better at international punctuation, |
| 302 // but this is what cricket did, and it should be removed |
| 303 // soon when gmail does the notification instead of us |
| 304 // forming it on the client. |
| 305 static void ShortenName(std::string* name) { |
| 306 size_t start = name->find_first_not_of(name_delim); |
| 307 if (start != std::string::npos && start > 0) { |
| 308 name->erase(0, start); |
| 309 } |
| 310 start = name->find_first_of(name_delim); |
| 311 if (start != std::string::npos) { |
| 312 name->erase(start); |
| 313 } |
| 314 } |
| 315 |
| 316 void UpdateName() { |
| 317 // Update the name if is going to be used. |
| 318 if (!visible_) { |
| 319 return; |
| 320 } |
| 321 |
| 322 if (me_) { |
| 323 name_ = "me"; |
| 324 return; |
| 325 } |
| 326 |
| 327 if (sender_.name().empty() && sender_.address().empty()) { |
| 328 name_ = ""; |
| 329 return; |
| 330 } |
| 331 |
| 332 name_ = sender_.name(); |
| 333 // Handle the case of no name or a name looks like an email address. |
| 334 // When mail is sent to "Quality@example.com" <quality-team@example.com>, |
| 335 // we shouldn't show "Quality@example.com" as the name. |
| 336 // Instead use the email address (without the @...) |
| 337 if (name_.empty() || name_.find_first_of("@") != std::string::npos) { |
| 338 name_ = sender_.address(); |
| 339 size_t at_index = name_.find_first_of("@"); |
| 340 if (at_index != std::string::npos) { |
| 341 name_.erase(at_index); |
| 342 } |
| 343 } else if (short_format_) { |
| 344 ShortenName(&name_); |
| 345 } |
| 346 |
| 347 if (name_.empty()) { |
| 348 name_ = "(unknown)"; |
| 349 } |
| 350 |
| 351 // Abbreviate if too long. |
| 352 if (name_.size() > space_) { |
| 353 name_.replace(space_ - 1, std::string::npos, "."); |
| 354 } |
| 355 } |
| 356 |
| 357 const MailSender& sender_; |
| 358 std::string name_; |
| 359 bool visible_; |
| 360 bool short_format_; |
| 361 size_t space_; |
| 362 bool me_; |
| 363 DISALLOW_COPY_AND_ASSIGN(SenderFormatter); |
| 364 }; |
| 365 |
| 366 const char kNormalSeparator[] = ", "; |
| 367 const char kEllidedSeparator[] = " .."; |
| 368 |
| 369 std::string FormatName(const std::string& name, bool bold) { |
| 370 std::string formatted_name; |
| 371 if (bold) { |
| 372 formatted_name.append("<b>"); |
| 373 } |
| 374 formatted_name.append(HtmlEncode(name)); |
| 375 if (bold) { |
| 376 formatted_name.append("</b>"); |
| 377 } |
| 378 return formatted_name; |
| 379 } |
| 380 |
| 381 class SenderFormatterList { |
| 382 public: |
| 383 // sender_list must not be deleted while this class is being used. |
| 384 SenderFormatterList(const MailSenderList& sender_list, |
| 385 const std::string& me_address) |
| 386 : state_(INITIAL_STATE), |
| 387 are_any_read_(false), |
| 388 index_(-1), |
| 389 first_unread_index_(-1) { |
| 390 // Add all read messages. |
| 391 const MailSender* originator = NULL; |
| 392 bool any_unread = false; |
| 393 for (size_t i = 0; i < sender_list.size(); ++i) { |
| 394 if (sender_list[i].originator()) { |
| 395 originator = &sender_list[i]; |
| 396 } |
| 397 if (sender_list[i].unread()) { |
| 398 any_unread = true; |
| 399 continue; |
| 400 } |
| 401 are_any_read_ = true; |
| 402 if (!sender_list[i].originator()) { |
| 403 senders_.push_back(new SenderFormatter(sender_list[i], |
| 404 me_address)); |
| 405 } |
| 406 } |
| 407 |
| 408 // There may not be an orignator (if there no senders). |
| 409 if (originator) { |
| 410 senders_.insert(senders_.begin(), new SenderFormatter(*originator, |
| 411 me_address)); |
| 412 } |
| 413 |
| 414 // Add all unread messages. |
| 415 if (any_unread) { |
| 416 // It should be rare, but there may be cases when all of the |
| 417 // senders appear to have read the message. |
| 418 first_unread_index_ = is_first_unread() ? 0 : senders_.size(); |
| 419 for (size_t i = 0; i < sender_list.size(); ++i) { |
| 420 if (!sender_list[i].unread()) { |
| 421 continue; |
| 422 } |
| 423 // Don't add the originator if it is already at the |
| 424 // start of the "unread" list. |
| 425 if (sender_list[i].originator() && is_first_unread()) { |
| 426 continue; |
| 427 } |
| 428 senders_.push_back(new SenderFormatter(sender_list[i], |
| 429 me_address)); |
| 430 } |
| 431 } |
| 432 } |
| 433 |
| 434 ~SenderFormatterList() { |
| 435 CleanupSequence(&senders_); |
| 436 } |
| 437 |
| 438 std::string GetHtml(int space) { |
| 439 index_ = -1; |
| 440 state_ = INITIAL_STATE; |
| 441 while (!added_.empty()) { |
| 442 added_.pop(); |
| 443 } |
| 444 |
| 445 // If there is only one sender, let it take up all of the space. |
| 446 if (senders_.size() == 1) { |
| 447 senders_[0]->set_space(space); |
| 448 senders_[0]->set_short_format(false); |
| 449 } |
| 450 |
| 451 int length = 1; |
| 452 // Add as many senders as we can in the given space. |
| 453 // Computes the visible length at each iteration, |
| 454 // but does not construct the actual html. |
| 455 while (length < space && AddNextSender()) { |
| 456 int new_length = ConstructHtml(is_first_unread(), NULL); |
| 457 // Remove names to avoid truncating |
| 458 // * if there will be at least 2 left or |
| 459 // * if the spacing <= 2 characters per sender and there |
| 460 // is at least one left. |
| 461 if ((new_length > space && |
| 462 (visible_count() > 2 || |
| 463 (ComputeSpacePerSender(space) <= 2 && visible_count() > 1)))) { |
| 464 RemoveLastAddedSender(); |
| 465 break; |
| 466 } |
| 467 length = new_length; |
| 468 } |
| 469 |
| 470 if (length > space) { |
| 471 int max = ComputeSpacePerSender(space); |
| 472 for (size_t i = 0; i < senders_.size(); ++i) { |
| 473 if (senders_[i]->visible()) { |
| 474 senders_[i]->set_space(max); |
| 475 } |
| 476 } |
| 477 } |
| 478 |
| 479 // Now construct the actual html |
| 480 std::string html_list; |
| 481 length = ConstructHtml(is_first_unread(), &html_list); |
| 482 if (length > space) { |
| 483 LOG(LS_WARNING) << "LENGTH: " << length << " exceeds " |
| 484 << space << " " << html_list; |
| 485 } |
| 486 return html_list; |
| 487 } |
| 488 |
| 489 private: |
| 490 int ComputeSpacePerSender(int space) const { |
| 491 // Why the "- 2"? To allow for the " .. " which may occur |
| 492 // after the names, and no matter what always allow at least |
| 493 // 2 characters per sender. |
| 494 return talk_base::_max<int>(space / visible_count() - 2, 2); |
| 495 } |
| 496 |
| 497 // Finds the next sender that should be added to the "from" list |
| 498 // and sets it to visible. |
| 499 // |
| 500 // This method may be called until it returns false or |
| 501 // until RemoveLastAddedSender is called. |
| 502 bool AddNextSender() { |
| 503 // The progression is: |
| 504 // 1. Add the person who started the thread, which is the first message. |
| 505 // 2. Add the first unread message (unless it has already been added). |
| 506 // 3. Add the last message (unless it has already been added). |
| 507 // 4. Add the message that is just before the last message processed |
| 508 // (unless it has already been added). |
| 509 // If there is no message (i.e. at index -1), return false. |
| 510 // |
| 511 // Typically, this method is called until it returns false or |
| 512 // all of the space available is used. |
| 513 switch (state_) { |
| 514 case INITIAL_STATE: |
| 515 state_ = FIRST_MESSAGE; |
| 516 index_ = 0; |
| 517 // If the server behaves odd and doesn't send us any senders, |
| 518 // do something graceful. |
| 519 if (senders_.size() == 0) { |
| 520 return false; |
| 521 } |
| 522 break; |
| 523 |
| 524 case FIRST_MESSAGE: |
| 525 if (first_unread_index_ >= 0) { |
| 526 state_ = FIRST_UNREAD_MESSAGE; |
| 527 index_ = first_unread_index_; |
| 528 break; |
| 529 } |
| 530 // fall through |
| 531 case FIRST_UNREAD_MESSAGE: |
| 532 state_ = LAST_MESSAGE; |
| 533 index_ = senders_.size() - 1; |
| 534 break; |
| 535 |
| 536 case LAST_MESSAGE: |
| 537 case PREVIOUS_MESSAGE: |
| 538 state_ = PREVIOUS_MESSAGE; |
| 539 index_--; |
| 540 break; |
| 541 |
| 542 case REMOVED_MESSAGE: |
| 543 default: |
| 544 ASSERT(false); |
| 545 return false; |
| 546 } |
| 547 |
| 548 if (index_ < 0) { |
| 549 return false; |
| 550 } |
| 551 |
| 552 if (!senders_[index_]->visible()) { |
| 553 added_.push(index_); |
| 554 senders_[index_]->set_visible(true); |
| 555 } |
| 556 return true; |
| 557 } |
| 558 |
| 559 // Makes the last added sender not visible. |
| 560 // |
| 561 // May be called while visible_count() > 0. |
| 562 void RemoveLastAddedSender() { |
| 563 state_ = REMOVED_MESSAGE; |
| 564 int index = added_.top(); |
| 565 added_.pop(); |
| 566 senders_[index]->set_visible(false); |
| 567 } |
| 568 |
| 569 // Constructs the html of the SenderList and returns the length of the |
| 570 // visible text. |
| 571 // |
| 572 // The algorithm simply walks down the list of Senders, appending |
| 573 // the html for each visible sender, and adding ellipsis or commas |
| 574 // in between, whichever is appropriate. |
| 575 // |
| 576 // html Filled with html. Maybe NULL if the html doesn't |
| 577 // need to be constructed yet (useful for simply |
| 578 // determining the length of the visible text). |
| 579 // |
| 580 // returns The approximate visible length of the html. |
| 581 int ConstructHtml(bool first_is_unread, |
| 582 std::string* html) const { |
| 583 if (senders_.empty()) { |
| 584 return 0; |
| 585 } |
| 586 |
| 587 int length = 0; |
| 588 |
| 589 // The first is always visible |
| 590 const SenderFormatter* sender = senders_[0]; |
| 591 const std::string& originator_name = sender->name(); |
| 592 length += originator_name.length(); |
| 593 if (html) { |
| 594 html->append(FormatName(originator_name, first_is_unread)); |
| 595 } |
| 596 |
| 597 bool elided = false; |
| 598 const char* between = ""; |
| 599 for (size_t i = 1; i < senders_.size(); i++) { |
| 600 sender = senders_[i]; |
| 601 |
| 602 if (sender->visible()) { |
| 603 // Handle the separator |
| 604 between = elided ? " " : kNormalSeparator; |
| 605 // Ignore the , for length because it is so narrow, |
| 606 // so in both cases above the space is the only things |
| 607 // that counts for spaces. |
| 608 length++; |
| 609 |
| 610 // Handle the name |
| 611 const std::string name = sender->name(); |
| 612 length += name.size(); |
| 613 |
| 614 // Construct the html |
| 615 if (html) { |
| 616 html->append(between); |
| 617 html->append(FormatName(name, sender->is_unread())); |
| 618 } |
| 619 elided = false; |
| 620 } else if (!elided) { |
| 621 between = kEllidedSeparator; |
| 622 length += 2; // ".." is narrow |
| 623 if (html) { |
| 624 html->append(between); |
| 625 } |
| 626 elided = true; |
| 627 } |
| 628 } |
| 629 return length; |
| 630 } |
| 631 |
| 632 bool is_first_unread() const { |
| 633 return !are_any_read_; |
| 634 } |
| 635 |
| 636 size_t visible_count() const { |
| 637 return added_.size(); |
| 638 } |
| 639 |
| 640 enum MessageState { |
| 641 INITIAL_STATE, |
| 642 FIRST_MESSAGE, |
| 643 FIRST_UNREAD_MESSAGE, |
| 644 LAST_MESSAGE, |
| 645 PREVIOUS_MESSAGE, |
| 646 REMOVED_MESSAGE, |
| 647 }; |
| 648 |
| 649 // What state we were in during the last "stateful" function call. |
| 650 MessageState state_; |
| 651 bool are_any_read_; |
| 652 std::vector<SenderFormatter*> senders_; |
| 653 std::stack<int> added_; |
| 654 int index_; |
| 655 int first_unread_index_; |
| 656 DISALLOW_COPY_AND_ASSIGN(SenderFormatterList); |
| 657 }; |
| 658 |
| 659 |
| 660 std::string GetSenderHtml(const MailSenderList& sender_list, |
| 661 int message_count, |
| 662 const std::string& me_address, |
| 663 int space) { |
| 664 // There has to be at least 9 spaces to show something reasonable. |
| 665 ASSERT(space >= 10); |
| 666 std::string count_html; |
| 667 if (message_count > 1) { |
| 668 std::string count(IntToString(message_count)); |
| 669 space -= (count.size()); |
| 670 count_html.append(" ("); |
| 671 count_html.append(count); |
| 672 count_html.append(")"); |
| 673 // Reduce space due to parenthesis and . |
| 674 space -= 2; |
| 675 } |
| 676 |
| 677 SenderFormatterList senders(sender_list, me_address); |
| 678 std::string html_list(senders.GetHtml(space)); |
| 679 html_list.append(count_html); |
| 680 return html_list; |
| 681 } |
| 682 } // namespace notifier |
OLD | NEW |