diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 049bba2eb4..d0949175a2 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -56,7 +56,8 @@ const CLOSED = "closed", categoryId: "topic.category.id", tags: "topic.tags", featuredLink: "topic.featured_link" - }; + }, + FAST_REPLY_LENGTH_THRESHOLD = 10000; export const SAVE_LABELS = { [EDIT]: "composer.save_edit", @@ -445,10 +446,62 @@ const Composer = RestModel.extend({ @computed("reply") replyLength(reply) { reply = reply || ""; - while (Quote.REGEXP.test(reply)) { - reply = reply.replace(Quote.REGEXP, ""); + + if (reply.length > FAST_REPLY_LENGTH_THRESHOLD) { + return reply.length; } - return reply.replace(/\s+/gim, " ").trim().length; + + if (Quote.REGEXP.test(reply)) { + // make it global so we can strip all quotes at once + const regex = new RegExp(Quote.REGEXP.source, "img"); + reply = reply.replace(regex, ""); + } + + // This is in place so we do not generate any intermediate + // strings while calculating the length, this is issued + // every keypress in the composer so it needs to be very fast + let len = 0, + skipSpace = true; + + for (let i = 0; i < reply.length; i++) { + const code = reply.charCodeAt(i); + + let isSpace = false; + if (code >= 0x2000 && code <= 0x200a) { + isSpace = true; + } else { + switch (code) { + case 0x09: // \t + case 0x0a: // \n + case 0x0b: // \v + case 0x0c: // \f + case 0x0d: // \r + case 0x20: + case 0xa0: + case 0x1680: + case 0x202f: + case 0x205f: + case 0x3000: + isSpace = true; + } + } + + if (isSpace) { + if (!skipSpace) { + len++; + skipSpace = true; + } + } else { + len++; + skipSpace = false; + } + } + + if (len > 0 && skipSpace) { + len--; + } + + return len; }, @on("init")