Fossil

Check-in [3449350042]
Login

Check-in [3449350042]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Add chat search for message by ID using search term #NNNN.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | fts5-chat-search
Files: files | file ages | folders
SHA3-256: 3449350042fa7fb74de4cb0b1829fc62f63b83c2ec204264f7d776eaa3a55865
User & Date: stephan 2024-07-02 10:33:23.428
Context
2024-07-02
11:03
If the chat search request has an HTTP error, report it. It currently fails without any useful information, beyond the HTTP code, if the query triggers a prepare() error, but that's at least no longer silently failing. ... (check-in: 5ae93fd23f user: stephan tags: fts5-chat-search)
10:33
Add chat search for message by ID using search term #NNNN. ... (check-in: 3449350042 user: stephan tags: fts5-chat-search)
09:22
/chat: use the historical timestamp format for main-feed messages and ISO-8601 for search results. Misc. internal cleanups. ... (check-in: 3c53bd325a user: stephan tags: fts5-chat-search)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/chat.c.
753
754
755
756
757
758
759


760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779



780







781
782
783
784
785

786
787
788

789
790
791
792
793
794
795


/*
** WEBPAGE: chat-query hidden loadavg-exempt
*/
void chat_query_webpage(void){
  Blob json;                  /* The json to be constructed and returned */


  int nLimit = atoi(PD("n","500"));
  const char *zQuery = PD("q", "");
  int iFirst = atoi(PD("i","0"));

  Blob sql = empty_blob;
  Stmt q1;
  i64 iMin = 0;
  i64 iMax = 0;

  login_check_credentials();
  if( !g.perm.Chat ) {
    chat_emit_permissions_error(1);
    return;
  }
  chat_create_tables();
  cgi_set_content_type("application/json");

  if( zQuery[0] ){
    iMax = db_int64(0, "SELECT max(msgid) FROM chat");
    iMin = db_int64(0, "SELECT min(msgid) FROM chat");



    blob_append_sql(&sql,







        "SELECT * FROM ("
        "SELECT c.msgid, datetime(c.mtime), c.xfrom, "
        "  highlight(chatfts1, 0, '<span class=\"match\">', '</span>'), "
        "  octet_length(c.file), c.fname, c.fmime, c.mdel, c.lmtime"
        "  FROM chatfts1(%Q) f, chat c WHERE f.rowid=c.msgid "

        "  ORDER BY f.rowid DESC LIMIT %d"
        ") ORDER BY 1 ASC", zQuery, nLimit
    );

  }else{
    blob_append_sql(&sql,
        "SELECT msgid, datetime(mtime), xfrom, "
        "  xmsg, octet_length(file), fname, fmime, mdel, lmtime"
        "  FROM chat WHERE msgid>=%d LIMIT %d",
        iFirst, nLimit
    );







>
>

<

|
<
<














>
>
>
|
>
>
>
>
>
>
>



|
|
>


|
>







753
754
755
756
757
758
759
760
761
762

763
764


765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806


/*
** WEBPAGE: chat-query hidden loadavg-exempt
*/
void chat_query_webpage(void){
  Blob json;                  /* The json to be constructed and returned */
  Blob sql = empty_blob;
  Stmt q1;
  int nLimit = atoi(PD("n","500"));

  int iFirst = atoi(PD("i","0"));
  const char *zQuery = PD("q", "");


  i64 iMin = 0;
  i64 iMax = 0;

  login_check_credentials();
  if( !g.perm.Chat ) {
    chat_emit_permissions_error(1);
    return;
  }
  chat_create_tables();
  cgi_set_content_type("application/json");

  if( zQuery[0] ){
    iMax = db_int64(0, "SELECT max(msgid) FROM chat");
    iMin = db_int64(0, "SELECT min(msgid) FROM chat");
    if( '#'==zQuery[0] ){
      /* Assume we're looking for an exact msgid match. */
      ++zQuery;
      blob_append_sql(&sql,
        "SELECT msgid, datetime(mtime), xfrom, "
        "  xmsg, octet_length(file), fname, fmime, mdel, lmtime "
        "  FROM chat WHERE msgid=+%Q",
        zQuery
      );
    }else{
      blob_append_sql(&sql,
        "SELECT * FROM ("
        "SELECT c.msgid, datetime(c.mtime), c.xfrom, "
        "  highlight(chatfts1, 0, '<span class=\"match\">', '</span>'), "
        "  octet_length(c.file), c.fname, c.fmime, c.mdel, c.lmtime "
        "  FROM chatfts1(%Q) f, chat c "
        "  WHERE f.rowid=c.msgid"
        "  ORDER BY f.rowid DESC LIMIT %d"
        ") ORDER BY 1 ASC", zQuery, nLimit
      );
    }
  }else{
    blob_append_sql(&sql,
        "SELECT msgid, datetime(mtime), xfrom, "
        "  xmsg, octet_length(file), fname, fmime, mdel, lmtime"
        "  FROM chat WHERE msgid>=%d LIMIT %d",
        iFirst, nLimit
    );
Changes to src/fossil.fetch.js.
232
233
234
235
236
237
238
239
240
241
242
243
244

/**
   urlTransform() must refer to a function which accepts a relative path
   to the same site as fetch() is served from and an optional set of
   URL parameters to pass with it (in the form a of a string
   ("a=b&c=d...") or an object of key/value pairs (which it converts
   to such a string), and returns the resulting URL or URI as a string.
*/  
fossil.fetch.urlTransform = (u,p)=>fossil.repoUrl(u,p);
fossil.fetch.beforesend = function(){};
fossil.fetch.aftersend = function(){};
fossil.fetch.timeout = 15000/* Default timeout, in ms. */;
})(window.fossil);







|





232
233
234
235
236
237
238
239
240
241
242
243
244

/**
   urlTransform() must refer to a function which accepts a relative path
   to the same site as fetch() is served from and an optional set of
   URL parameters to pass with it (in the form a of a string
   ("a=b&c=d...") or an object of key/value pairs (which it converts
   to such a string), and returns the resulting URL or URI as a string.
*/
fossil.fetch.urlTransform = (u,p)=>fossil.repoUrl(u,p);
fossil.fetch.beforesend = function(){};
fossil.fetch.aftersend = function(){};
fossil.fetch.timeout = 15000/* Default timeout, in ms. */;
})(window.fossil);
Changes to src/fossil.page.chat.js.
174
175
176
177
178
179
180

181
182
183
184
185
186

187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
      },
      filterState:{
        activeUser: undefined,
        match: function(uname){
          return this.activeUser===uname || !this.activeUser;
        }
      },

      /** Gets (no args) or sets (1 arg) the current input text field
          value, taking into account single- vs multi-line input. The
          getter returns a string and the setter returns this
          object. As a special case, if arguments[0] is a boolean
          value, it behaves like a getter and, if arguments[0]===true
          it clears the input field before returning. */

      inputValue: function(/*string newValue | bool clearInputField*/){
        const e = this.inputElement();
        if(arguments.length && 'boolean'!==typeof arguments[0]){
          if(e.isContentEditable) e.innerText = arguments[0];
          else e.value = arguments[0];
          return this;
        }
        const rc = e.isContentEditable ? e.innerText : e.value;
        if( true===arguments[0] ){
          if(e.isContentEditable) e.innerText = '';
          else e.value = '';
        }
        return rc;
      },
      /** Asks the current user input field to take focus. Returns this. */
      inputFocus: function(){
        this.inputElement().focus();
        return this;
      },
      /** Returns the current message input element. */







>
|
|
|
|
|
|
>












|







174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
      },
      filterState:{
        activeUser: undefined,
        match: function(uname){
          return this.activeUser===uname || !this.activeUser;
        }
      },
      /**
         Gets (no args) or sets (1 arg) the current input text field
         value, taking into account single- vs multi-line input. The
         getter returns a trim()'d string and the setter returns this
         object. As a special case, if arguments[0] is a boolean
         value, it behaves like a getter and, if arguments[0]===true
         it clears the input field before returning.
      */
      inputValue: function(/*string newValue | bool clearInputField*/){
        const e = this.inputElement();
        if(arguments.length && 'boolean'!==typeof arguments[0]){
          if(e.isContentEditable) e.innerText = arguments[0];
          else e.value = arguments[0];
          return this;
        }
        const rc = e.isContentEditable ? e.innerText : e.value;
        if( true===arguments[0] ){
          if(e.isContentEditable) e.innerText = '';
          else e.value = '';
        }
        return rc && rc.trim();
      },
      /** Asks the current user input field to take focus. Returns this. */
      inputFocus: function(){
        this.inputElement().focus();
        return this;
      },
      /** Returns the current message input element. */
2376
2377
2378
2379
2380
2381
2382
2383

2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396


2397
2398
2399
2400
2401
2402
2403
2404
2405
2406

     Returns the DOM element which wraps all of the chat search
     result elements.
  */
  Chat.clearSearch = function(addInstructions=false){
    const e = D.clearElement( this.e.searchContent );
    if(addInstructions){
      D.append(e, "Enter search terms in the message field.");

    }
    return e;
  };
  Chat.clearSearch(true);
  /**
     Submits a history search using the main input field's current
     text. It is assumed that Chat.e.viewSearch===Chat.e.currentView.
  */
  Chat.submitSearch = function(){
    const term = this.inputValue(true);
    const eMsgTgt = this.clearSearch(true);
    if( !term ) return;
    D.append( eMsgTgt, "Searching for ",term," ...");


    F.fetch(
      "chat-query", {
        urlParams: {q: term},
        responseType: 'json',
        onload:function(jx){
          let previd = 0;
          D.clearElement(eMsgTgt);
          jx.msgs.forEach((m)=>{
            m.isSearchResult = true;
            const mw = new Chat.MessageWidget(m);







|
>













>
>


|







2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411

     Returns the DOM element which wraps all of the chat search
     result elements.
  */
  Chat.clearSearch = function(addInstructions=false){
    const e = D.clearElement( this.e.searchContent );
    if(addInstructions){
      D.append(e, "Enter search terms in the message field. "+
               "Use #NNNNN to search for the message with ID NNNNN.");
    }
    return e;
  };
  Chat.clearSearch(true);
  /**
     Submits a history search using the main input field's current
     text. It is assumed that Chat.e.viewSearch===Chat.e.currentView.
  */
  Chat.submitSearch = function(){
    const term = this.inputValue(true);
    const eMsgTgt = this.clearSearch(true);
    if( !term ) return;
    D.append( eMsgTgt, "Searching for ",term," ...");
    const fd = new FormData();
    fd.set('q', term);
    F.fetch(
      "chat-query", {
        payload: fd,
        responseType: 'json',
        onload:function(jx){
          let previd = 0;
          D.clearElement(eMsgTgt);
          jx.msgs.forEach((m)=>{
            m.isSearchResult = true;
            const mw = new Chat.MessageWidget(m);