public class ActionEditText extends IRCEditText {
    private DrawerLayout mDrawerLayout = null;

    public ActionEditText(Context context) {
        super(context);
        init();
    }

    public ActionEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ActionEditText(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        InputConnection ic = super.onCreateInputConnection(outAttrs);
        if(ic == null)
            return null;

        outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
        outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NAVIGATE_NEXT;
        if (IRCCloudApplication.getInstance().getApplicationContext().getResources().getBoolean(R.bool.isTablet) || PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("kb_send", false)) {
            outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
            outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
        } else {
            outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT | EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE;
            outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
        }
        if (PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("kb_caps", true)) {
            outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
        } else {
            outAttrs.inputType &= ~EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
        }

        EditorInfoCompat.setContentMimeTypes(outAttrs, new String[]{"image/*"});

        final InputConnectionCompat.OnCommitContentListener callback =
                new InputConnectionCompat.OnCommitContentListener() {
                    @Override
                    public boolean onCommitContent(InputContentInfoCompat inputContentInfo,
                                                   int flags, Bundle opts) {
                        // read and display inputContentInfo asynchronously
                        if ((flags &
                                InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
                            try {
                                inputContentInfo.requestPermission();
                            }
                            catch (Exception e) {
                                return false; // return false if failed
                            }
                        }

                        if(imageListener != null) {
                            boolean result = imageListener.onIMEImageReceived(inputContentInfo);
                            inputContentInfo.releasePermission();
                            return result;
                        }
                        return false;
                    }
                };
        return InputConnectionCompat.createWrapper(ic, outAttrs, callback);
    }

    private void init() {
    }

    @Override
    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
        if (mDrawerLayout != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
            mDrawerLayout.closeDrawers();
        }
        return super.onKeyPreIme(keyCode, event);
    }

    public void setDrawerLayout(DrawerLayout view) {
        mDrawerLayout = view;
    }

    private OnIMEImageReceivedListener imageListener = null;

    public void setOnIMEImageReceivedListener(OnIMEImageReceivedListener listener) {
        imageListener = listener;
    }

    public interface OnIMEImageReceivedListener {
        boolean onIMEImageReceived(InputContentInfoCompat info);
    }
} public abstract class AsyncTaskEx<Params, Progress, Result> {
    private static final String LOG_TAG = "AsyncTaskEx";

    private static final int CORE_POOL_SIZE = 10;
    private static final int MAXIMUM_POOL_SIZE = 50;
    private static final int KEEP_ALIVE = 10;

    private static final LinkedBlockingQueue<Runnable> sWorkQueue =
            new LinkedBlockingQueue<Runnable>();

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTaskEx #" + mCount.getAndIncrement());
        }
    };

    private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
            MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory);

    private static final int MESSAGE_POST_RESULT = 0x1;
    private static final int MESSAGE_POST_PROGRESS = 0x2;
    private static final int MESSAGE_POST_CANCEL = 0x3;

    private static final InternalHandler sHandler = new InternalHandler();

    private final WorkerRunnable<Params, Result> mWorker;
    private final FutureTask<Result> mFuture;

    private volatile Status mStatus = Status.PENDING;

    /**
     * Indicates the current status of the task. Each status will be set only once
     * during the lifetime of a task.
     */
    public enum Status {
        /**
         * Indicates that the task has not been executed yet.
         */
        PENDING,
        /**
         * Indicates that the task is running.
         */
        RUNNING,
        /**
         * Indicates that {@link AsyncTaskEx#onPostExecute} has finished.
         */
        FINISHED,
    }

    public static void clearQueue() {
        sWorkQueue.clear();
    }

    /**
     * Creates a new asynchronous task. This constructor must be invoked on the UI thread.
     */
    public AsyncTaskEx() {
        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                return doInBackground(mParams);
            }
        };

        mFuture = new FutureTask<Result>(mWorker) {
            @SuppressWarnings("unchecked")
            @Override
            protected void done() {
                Message message;
                Result result = null;

                try {
                    result = get();
                } catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("An error occured while executing doInBackground()",
                            e.getCause());
                } catch (CancellationException e) {
                    message = sHandler.obtainMessage(MESSAGE_POST_CANCEL,
                            new AsyncTaskExResult<Result>(AsyncTaskEx.this, (Result[]) null));
                    message.sendToTarget();
                    return;
                } catch (Throwable t) {
                    throw new RuntimeException("An error occured while executing "
                            + "doInBackground()", t);
                }

                message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
                        new AsyncTaskExResult<Result>(AsyncTaskEx.this, result));
                message.sendToTarget();
            }
        };
    }

    /**
     * Returns the current status of this task.
     *
     * @return The current status.
     */
    public final Status getStatus() {
        return mStatus;
    }

    /**
     * Override this method to perform a computation on a background thread. The
     * specified parameters are the parameters passed to {@link #execute}
     * by the caller of this task.
     * <p/>
     * This method can call {@link #publishProgress} to publish updates
     * on the UI thread.
     *
     * @param params The parameters of the task.
     * @return A result, defined by the subclass of this task.
     * @see #onPreExecute()
     * @see #onPostExecute
     * @see #publishProgress
     */
    protected abstract Result doInBackground(Params... params);

    /**
     * Runs on the UI thread before {@link #doInBackground}.
     *
     * @see #onPostExecute
     * @see #doInBackground
     */
    protected void onPreExecute() {
    }

    /**
     * Runs on the UI thread after {@link #doInBackground}. The
     * specified result is the value returned by {@link #doInBackground}
     * or null if the task was cancelled or an exception occured.
     *
     * @param result The result of the operation computed by {@link #doInBackground}.
     * @see #onPreExecute
     * @see #doInBackground
     */
    protected void onPostExecute(Result result) {
    }

    /**
     * Runs on the UI thread after {@link #publishProgress} is invoked.
     * The specified values are the values passed to {@link #publishProgress}.
     *
     * @param values The values indicating progress.
     * @see #publishProgress
     * @see #doInBackground
     */
    protected void onProgressUpdate(Progress... values) {
    }

    /**
     * Runs on the UI thread after {@link #cancel(boolean)} is invoked.
     *
     * @see #cancel(boolean)
     * @see #isCancelled()
     */
    protected void onCancelled() {
    }

    /**
     * Returns <tt>true</tt> if this task was cancelled before it completed
     * normally.
     *
     * @return <tt>true</tt> if task was cancelled before it completed
     * @see #cancel(boolean)
     */
    public final boolean isCancelled() {
        return mFuture.isCancelled();
    }

    /**
     * Attempts to cancel execution of this task.  This attempt will
     * fail if the task has already completed, already been cancelled,
     * or could not be cancelled for some other reason. If successful,
     * and this task has not started when <tt>cancel</tt> is called,
     * this task should never run.  If the task has already started,
     * then the <tt>mayInterruptIfRunning</tt> parameter determines
     * whether the thread executing this task should be interrupted in
     * an attempt to stop the task.
     *
     * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
     *                              task should be interrupted; otherwise, in-progress tasks are allowed
     *                              to complete.
     * @return <tt>false</tt> if the task could not be cancelled,
     * typically because it has already completed normally;
     * <tt>true</tt> otherwise
     * @see #isCancelled()
     * @see #onCancelled()
     */
    public final boolean cancel(boolean mayInterruptIfRunning) {
        return mFuture.cancel(mayInterruptIfRunning);
    }

    /**
     * Waits if necessary for the computation to complete, and then
     * retrieves its result.
     *
     * @return The computed result.
     * @throws CancellationException If the computation was cancelled.
     * @throws ExecutionException    If the computation threw an exception.
     * @throws InterruptedException  If the current thread was interrupted
     *                               while waiting.
     */
    public final Result get() throws InterruptedException, ExecutionException {
        return mFuture.get();
    }

    /**
     * Waits if necessary for at most the given time for the computation
     * to complete, and then retrieves its result.
     *
     * @param timeout Time to wait before cancelling the operation.
     * @param unit    The time unit for the timeout.
     * @return The computed result.
     * @throws CancellationException If the computation was cancelled.
     * @throws ExecutionException    If the computation threw an exception.
     * @throws InterruptedException  If the current thread was interrupted
     *                               while waiting.
     * @throws TimeoutException      If the wait timed out.
     */
    public final Result get(long timeout, TimeUnit unit) throws InterruptedException,
            ExecutionException, TimeoutException {
        return mFuture.get(timeout, unit);
    }

    /**
     * Executes the task with the specified parameters. The task returns
     * itself (this) so that the caller can keep a reference to it.
     * <p/>
     * This method must be invoked on the UI thread.
     *
     * @param params The parameters of the task.
     * @return This instance of AsyncTaskEx.
     * @throws IllegalStateException If {@link #getStatus()} returns either
     *                               {@link AsyncTaskEx.Status#RUNNING} or {@link AsyncTaskEx.Status#FINISHED}.
     */
    public final AsyncTaskEx<Params, Progress, Result> execute(Params... params) {
        if (mStatus != Status.PENDING) {
            switch (mStatus) {
                case RUNNING:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task is already running.");
                case FINISHED:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task has already been executed "
                            + "(a task can be executed only once)");
            }
        }

        mStatus = Status.RUNNING;

        onPreExecute();

        mWorker.mParams = params;
        sExecutor.execute(mFuture);

        return this;
    }

    /**
     * This method can be invoked from {@link #doInBackground} to
     * publish updates on the UI thread while the background computation is
     * still running. Each call to this method will trigger the execution of
     * {@link #onProgressUpdate} on the UI thread.
     *
     * @param values The progress values to update the UI with.
     * @see #onProgressUpdate
     * @see #doInBackground
     */
    protected final void publishProgress(Progress... values) {
        sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
                new AsyncTaskExResult<Progress>(this, values)).sendToTarget();
    }

    private void finish(Result result) {
        onPostExecute(result);
        mStatus = Status.FINISHED;
    }

    private static class InternalHandler extends Handler {
        public InternalHandler() {
            super(Looper.getMainLooper());
        }

        @SuppressWarnings({"rawtypes", "unchecked"})
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskExResult result = (AsyncTaskExResult) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    // There is only one result
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
                case MESSAGE_POST_CANCEL:
                    result.mTask.onCancelled();
                    break;
            }
        }
    }

    private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
        Params[] mParams;
    }

    private static class AsyncTaskExResult<Data> {
        @SuppressWarnings("rawtypes")
        final AsyncTaskEx mTask;
        final Data[] mData;

        @SuppressWarnings("rawtypes")
        AsyncTaskExResult(AsyncTaskEx task, Data... data) {
            mTask = task;
            mData = data;
        }
    }
} public class BackgroundTaskWorker extends Worker {
    @Dao
    public interface BackgroundTasksDao {
        @Query("SELECT * FROM BackgroundTask")
        List<BackgroundTask> getBackgroundTasks();

        @Query("SELECT * FROM BackgroundTask WHERE type = :type")
        List<BackgroundTask> getBackgroundTasks(int type);

        @Query("SELECT * FROM BackgroundTask WHERE type = :type AND data = :data")
        List<BackgroundTask> getBackgroundTasks(int type, String data);

        @Query("SELECT * FROM BackgroundTask WHERE tag = :tag LIMIT 1")
        BackgroundTask getBackgroundTask(String tag);

        @Insert(onConflict = OnConflictStrategy.REPLACE)
        void insert(BackgroundTask backgroundTask);

        @Update
        void update(BackgroundTask backgroundTask);

        @Delete
        void delete(BackgroundTask backgroundTask);
    }

    public BackgroundTaskWorker(@NonNull Context appContext, @NonNull WorkerParameters params) {
        super(appContext, params);
    }

    public static void registerGCM(String token) {
        List<BackgroundTask> tasks = IRCCloudDatabase.getInstance().BackgroundTasksDao().getBackgroundTasks(BackgroundTask.TYPE_FCM_REGISTER);
        for(BackgroundTask t : tasks) {
            IRCCloudLog.Log(Log.INFO, "IRCCloud", "Removing old FCM registration task: " + t.getTag());
            try {
                WorkManager.getInstance(IRCCloudApplication.getInstance().getApplicationContext()).cancelUniqueWork(t.getTag());
            } catch (Exception e) {
            }
            IRCCloudDatabase.getInstance().BackgroundTasksDao().delete(t);
        }

        if(NetworkConnection.getInstance().session != null && NetworkConnection.getInstance().session.length() > 0) {
            BackgroundTask task = new BackgroundTask();
            task.setType(BackgroundTask.TYPE_FCM_REGISTER);
            task.setData(token);
            task.setTag(Long.toString(System.currentTimeMillis()));
            task.setSession(NetworkConnection.getInstance().session);

            IRCCloudLog.Log(Log.INFO, "IRCCloud", "Scheduled FCM registration task: " + task.getTag());
            try {
                OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(BackgroundTaskWorker.class)
                        .setInputData(new Data.Builder().putString("tag", task.getTag()).build())
                        .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
                        .build();
                WorkManager.getInstance(IRCCloudApplication.getInstance().getApplicationContext()).enqueueUniqueWork(task.getTag(), ExistingWorkPolicy.REPLACE, request);
            } catch (Exception e) {
                return;
            }
            IRCCloudDatabase.getInstance().BackgroundTasksDao().insert(task);
        }
    }

    private static void scheduleUnregister(String token, String session) {
        if(token != null && token.length() > 0) {
            List<BackgroundTask> tasks = IRCCloudDatabase.getInstance().BackgroundTasksDao().getBackgroundTasks(BackgroundTask.TYPE_FCM_REGISTER, token);

            for(BackgroundTask t : tasks) {
                try {
                    WorkManager.getInstance(IRCCloudApplication.getInstance().getApplicationContext()).cancelUniqueWork(t.getTag());
                } catch (Exception e) {
                }
                IRCCloudDatabase.getInstance().BackgroundTasksDao().delete(t);
            }

            BackgroundTask task = new BackgroundTask();
            task.setType(BackgroundTask.TYPE_FCM_UNREGISTER);
            task.setTag(Long.toString(System.currentTimeMillis()));
            task.setData(token);
            task.setSession(session);

            try {
                OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(BackgroundTaskWorker.class)
                        .setInputData(new Data.Builder().putString("tag", task.getTag()).build())
                        .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
                        .build();
                WorkManager.getInstance(IRCCloudApplication.getInstance().getApplicationContext()).enqueueUniqueWork(task.getTag(), ExistingWorkPolicy.REPLACE, request);
            } catch (Exception e) {
                return;
            }
            IRCCloudDatabase.getInstance().BackgroundTasksDao().insert(task);
        }
    }

    public static void unregisterGCM() {
        final String session = NetworkConnection.getInstance().session;
        final String token = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getString("gcm_token", null);

        if(token == null)
            return;

        List<BackgroundTask> tasks = IRCCloudDatabase.getInstance().BackgroundTasksDao().getBackgroundTasks(BackgroundTask.TYPE_FCM_REGISTER, token);

        for(BackgroundTask t : tasks) {
            try {
                WorkManager.getInstance(IRCCloudApplication.getInstance().getApplicationContext()).cancelUniqueWork(t.getTag());
            } catch (Exception e) {
            }
            IRCCloudDatabase.getInstance().BackgroundTasksDao().delete(t);
        }

        if(Looper.myLooper() == Looper.getMainLooper()) {
            new AsyncTask<Void, Void, Void>() {

                @Override
                protected Void doInBackground(Void... voids) {
                    FirebaseMessaging.getInstance().deleteToken();
                    if(!onGcmUnregister(token, session).equals(Result.success()))
                        scheduleUnregister(token, session);
                    return null;
                }
            }.execute((Void) null);
        } else {
            FirebaseMessaging.getInstance().deleteToken();
            if(!onGcmUnregister(token, session).equals(Result.success()))
                scheduleUnregister(token, session);
        }
    }

    @Override
    public ListenableWorker.Result doWork() {
        String tag = getInputData().getString("tag");
        IRCCloudLog.Log(Log.INFO, "IRCCloud", "Executing background task with tag: " + tag);
        BackgroundTask task = IRCCloudDatabase.getInstance().BackgroundTasksDao().getBackgroundTask(tag);
        if(task != null) {
            switch(task.getType()) {
                case BackgroundTask.TYPE_FCM_REGISTER:
                    return onGcmRegister(task);
                case BackgroundTask.TYPE_FCM_UNREGISTER:
                    Result result = onGcmUnregister(task.getData(), task.getSession());
                    if(!result.equals(Result.retry()))
                        IRCCloudDatabase.getInstance().BackgroundTasksDao().delete(task);
                    return result;
            }
        } else {
            IRCCloudLog.Log(Log.ERROR, "IRCCloud", "Task not found");
        }

        return Result.failure();
    }

    private Result onGcmRegister(BackgroundTask task) {
        try {
            if(task.getSession() == null || task.getSession().length() == 0)
                return Result.failure();

            IRCCloudLog.Log(Log.INFO, "IRCCloud", "Registering for FCM");
            String token = task.getData();
            if(token != null && token.length() > 0) {
                JSONObject result = NetworkConnection.getInstance().registerGCM(token, task.getSession());
                if (result != null && result.has("success")) {
                    if(result.getBoolean("success")) {
                        IRCCloudLog.Log(Log.INFO, "IRCCloud", "Device successfully registered");
                        IRCCloudDatabase.getInstance().BackgroundTasksDao().delete(task);
                        return Result.success();
                    } else {
                        IRCCloudLog.Log(Log.ERROR, "IRCCloud", "Unable to register device: " + result.toString());
                        return Result.retry();
                    }
                } else {
                    IRCCloudLog.Log(Log.INFO, "IRCCloud", "Rescheduling FCM registration");
                    return Result.retry();
                }
            }
        } catch (Exception e) {
            NetworkConnection.printStackTraceToCrashlytics(e);
        }
        IRCCloudLog.Log(Log.ERROR, "IRCCloud", "FCM registration failed");
        IRCCloudDatabase.getInstance().BackgroundTasksDao().delete(task);

        return Result.failure();
    }

    private static Result onGcmUnregister(final String token, String session) {
        if(token != null) {
            try {
                IRCCloudLog.Log(Log.INFO, "IRCCloud", "Unregistering FCM");
                try {
                    FirebaseMessaging.getInstance().getToken().addOnSuccessListener(new OnSuccessListener<String>() {
                        @Override
                        public void onSuccess(@NonNull String s) {
                            if (token.equals(s)) {
                                IRCCloudLog.Log(Log.INFO, "IRCCloud", "Deleting old FCM token");
                                FirebaseMessaging.getInstance().deleteToken();
                            }
                        }
                    });
                } catch (Exception e) {
                    NetworkConnection.printStackTraceToCrashlytics(e);
                }
                JSONObject result = NetworkConnection.getInstance().unregisterGCM(token, session);
                if (result != null && result.has("success")) {
                    if (result.getBoolean("success") || result.getString("message").equals("auth")) {
                        IRCCloudLog.Log(Log.INFO, "IRCCloud", "Device successfully unregistered");
                        SharedPreferences.Editor e = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit();
                        e.remove(session);
                        e.apply();
                        return Result.success();
                    } else {
                        IRCCloudLog.Log(Log.ERROR, "IRCCloud", "Unable to unregister device: " + result.toString());
                        return Result.retry();
                    }
                } else {
                    IRCCloudLog.Log(Log.INFO, "IRCCloud", "Rescheduling FCM unregistration");
                    return Result.retry();
                }
            } catch (Exception e) {
                NetworkConnection.printStackTraceToCrashlytics(e);
            }
            IRCCloudLog.Log(Log.ERROR, "IRCCloud", "FCM unregistration failed");
        }

        return Result.failure();
    }
} public class CollapsedEventsList {
    public static final int TYPE_NETSPLIT = -1;
    public static final int TYPE_JOIN = 0;
    public static final int TYPE_PART = 1;
    public static final int TYPE_QUIT = 2;
    public static final int TYPE_MODE = 3;
    public static final int TYPE_POPIN = 4;
    public static final int TYPE_POPOUT = 5;
    public static final int TYPE_NICKCHANGE = 6;
    public static final int TYPE_CONNECTIONSTATUS = 7;

    public static final int MODE_OPER = 0;
    public static final int MODE_OWNER = 1;
    public static final int MODE_ADMIN = 2;
    public static final int MODE_OP = 3;
    public static final int MODE_HALFOP = 4;
    public static final int MODE_VOICE = 5;

    public static final int MODE_DEOPER = 6;
    public static final int MODE_DEOWNER = 7;
    public static final int MODE_DEADMIN = 8;
    public static final int MODE_DEOP = 9;
    public static final int MODE_DEHALFOP = 10;
    public static final int MODE_DEVOICE = 11;

    public static final int MODE_COUNT = 12;

    public boolean showChan = false;
    public boolean noColor = false;

    private String collapsedNickColor = null;

    public class CollapsedEvent {
        long eid;
        int type;
        boolean modes[] = new boolean[MODE_COUNT];
        String nick;
        String display_name;
        String old_nick;
        String hostmask;
        String msg;
        String from_mode;
        String from_nick;
        String target_mode;
        String chan;
        boolean operIsLower;
        int count;

        public String toString() {
            return "{type: " + type + ", nick: " + nick + ", old_nick: " + old_nick + ", hostmask: " + hostmask + ", msg: " + msg + ", chan: " + chan + "}";
        }

        public int modeCount() {
            int count = 0;
            for (int i = 0; i < MODE_COUNT; i++) {
                if (modes[i])
                    count++;
            }
            return count;
        }

        public boolean addMode(String mode) {
            if (mode.equals(server != null ? server.MODE_OPER.toLowerCase() : "y")) {
                operIsLower = true;
            }
            mode = mode.toLowerCase();

            if (mode.equals(server != null ? server.MODE_OPER.toLowerCase() : "y")) {
                if (modes[MODE_DEOPER])
                    modes[MODE_DEOPER] = false;
                else
                    modes[MODE_OPER] = true;
            } else if (mode.equals(server != null ? server.MODE_OWNER.toLowerCase() : "q")) {
                if (modes[MODE_DEOWNER])
                    modes[MODE_DEOWNER] = false;
                else
                    modes[MODE_OWNER] = true;
            } else if (mode.equals(server != null ? server.MODE_ADMIN.toLowerCase() : "a")) {
                if (modes[MODE_DEADMIN])
                    modes[MODE_DEADMIN] = false;
                else
                    modes[MODE_ADMIN] = true;
            } else if (mode.equals(server != null ? server.MODE_OP.toLowerCase() : "o")) {
                if (modes[MODE_DEOP])
                    modes[MODE_DEOP] = false;
                else
                    modes[MODE_OP] = true;
            } else if (mode.equals(server != null ? server.MODE_HALFOP.toLowerCase() : "h")) {
                if (modes[MODE_DEHALFOP])
                    modes[MODE_DEHALFOP] = false;
                else
                    modes[MODE_HALFOP] = true;
            } else if (mode.equals(server != null ? server.MODE_VOICED.toLowerCase() : "v")) {
                if (modes[MODE_DEVOICE])
                    modes[MODE_DEVOICE] = false;
                else
                    modes[MODE_VOICE] = true;
            } else {
                return false;
            }
            if (modeCount() == 0)
                return addMode(mode);
            return true;
        }

        public boolean removeMode(String mode) {
            mode = mode.toLowerCase();

            if (mode.equals(server != null ? server.MODE_OPER.toLowerCase() : "y")) {
                if (modes[MODE_OPER])
                    modes[MODE_OPER] = false;
                else
                    modes[MODE_DEOPER] = true;
            } else if (mode.equals(server != null ? server.MODE_OWNER.toLowerCase() : "q")) {
                if (modes[MODE_OWNER])
                    modes[MODE_OWNER] = false;
                else
                    modes[MODE_DEOWNER] = true;
            } else if (mode.equals(server != null ? server.MODE_ADMIN.toLowerCase() : "a")) {
                if (modes[MODE_ADMIN])
                    modes[MODE_ADMIN] = false;
                else
                    modes[MODE_DEADMIN] = true;
            } else if (mode.equals(server != null ? server.MODE_OP.toLowerCase() : "o")) {
                if (modes[MODE_OP])
                    modes[MODE_OP] = false;
                else
                    modes[MODE_DEOP] = true;
            } else if (mode.equals(server != null ? server.MODE_HALFOP.toLowerCase() : "h")) {
                if (modes[MODE_HALFOP])
                    modes[MODE_HALFOP] = false;
                else
                    modes[MODE_DEHALFOP] = true;
            } else if (mode.equals(server != null ? server.MODE_VOICED.toLowerCase() : "v")) {
                if (modes[MODE_VOICE])
                    modes[MODE_VOICE] = false;
                else
                    modes[MODE_DEVOICE] = true;
            } else {
                return false;
            }
            if (modeCount() == 0)
                return removeMode(mode);
            return true;
        }

        public String getModes(boolean showSymbol) {
            final String[] mode_msgs = {
                    "promoted to oper",
                    "promoted to owner",
                    "promoted to admin",
                    "opped",
                    "halfopped",
                    "voiced",
                    "demoted from oper",
                    "demoted from owner",
                    "demoted from admin",
                    "de-opped",
                    "de-halfopped",
                    "de-voiced",
            };

            String output = null;
            if (modeCount() > 0) {
                output = "";

                for (int i = 0; i < MODE_COUNT; i++) {
                    if (modes[i]) {
                        if (output.length() > 0)
                            output += ", ";
                        output += mode_msgs[i];
                        if (showSymbol) {
                            output += " (\u0004" + mode_colors.get(mode_modes[i].substring(1).toLowerCase()) + ((i == MODE_OPER && operIsLower)?mode_modes[i].toLowerCase():mode_modes[i]) + "\u000f";
                            if(ColorScheme.getInstance().theme != null)
                                output += "\u0004" + Integer.toHexString(ColorScheme.getInstance().messageTextColor).substring(2);
                            output += ")";
                        }
                    }
                }
            }

            return output;
        }
    }

    public class comparator implements Comparator<CollapsedEvent> {
        public int compare(CollapsedEvent e1, CollapsedEvent e2) {
            if (e1.type == e2.type) {
                if (e1.eid > e2.eid)
                    return 1;
                else
                    return -1;
            } else if (e1.type > e2.type) {
                return 1;
            } else {
                return -1;
            }
        }
    }

    private ArrayList<CollapsedEvent> data = new ArrayList<CollapsedEvent>();
    private Server server;
    private HashMap<String, String> mode_colors;
    private String mode_modes[];

    public CollapsedEventsList() {
        setServer(null);
    }

    public void setServer(Server s) {
        server = s;
        if (server != null) {
            mode_colors = new HashMap<String, String>() {{
                if(server.MODE_OPER.length() > 0)
                    put(server.MODE_OPER.toLowerCase(), "E02305");
                put(server.MODE_OWNER.toLowerCase(), "E7AA00");
                put(server.MODE_ADMIN.toLowerCase(), "6500A5");
                put(server.MODE_OP.toLowerCase(), "BA1719");
                put(server.MODE_HALFOP.toLowerCase(), "B55900");
                put(server.MODE_VOICED.toLowerCase(), "25B100");
            }};
            mode_modes = new String[]{
                    "+" + server.MODE_OPER,
                    "+" + server.MODE_OWNER,
                    "+" + server.MODE_ADMIN,
                    "+" + server.MODE_OP,
                    "+" + server.MODE_HALFOP,
                    "+" + server.MODE_VOICED,
                    "-" + server.MODE_OPER,
                    "-" + server.MODE_OWNER,
                    "-" + server.MODE_ADMIN,
                    "-" + server.MODE_OP,
                    "-" + server.MODE_HALFOP,
                    "-" + server.MODE_VOICED
            };
        } else {
            mode_colors = new HashMap<String, String>() {{
                put("y", "E02305");
                put("q", "E7AA00");
                put("a", "6500A5");
                put("o", "BA1719");
                put("h", "B55900");
                put("v", "25B100");
            }};
            mode_modes = new String[]{
                    "+Y", "+q", "+a", "+o", "+h", "+v", "-Y", "-q", "-a", "-o", "-h", "-v"
            };
        }
    }

    public String toString() {
        String out = "CollapsedEventsList {\n";
        for (int i = 0; i < data.size(); i++) {
            out += "\t" + data.get(i).toString() + "\n";
        }
        out += "}";
        return out;
    }

    public boolean addEvent(Event event) {
        String type = event.type;
        if (type.startsWith("you_"))
            type = type.substring(4);

        if (type.equalsIgnoreCase("joined_channel")) {
            addEvent(event.eid, CollapsedEventsList.TYPE_JOIN, event.nick, null, event.hostmask, event.from_mode, null, event.chan, event.from);
        } else if (type.equalsIgnoreCase("parted_channel")) {
            addEvent(event.eid, CollapsedEventsList.TYPE_PART, event.nick, null, event.hostmask, event.from_mode, event.msg, event.chan, event.from);
        } else if (type.equalsIgnoreCase("quit")) {
            addEvent(event.eid, CollapsedEventsList.TYPE_QUIT, event.nick, null, event.hostmask, event.from_mode, event.msg, event.chan, event.from);
        } else if (type.equalsIgnoreCase("nickchange")) {
            addEvent(event.eid, CollapsedEventsList.TYPE_NICKCHANGE, event.nick, event.old_nick, null, event.from_mode, null, event.chan, event.from);
        } else if (type.equalsIgnoreCase("socket_closed") || type.equalsIgnoreCase("connecting_failed") || type.equalsIgnoreCase("connecting_cancelled")) {
            addEvent(event.eid, CollapsedEventsList.TYPE_CONNECTIONSTATUS, null, null, null, null, event.msg, null, null);
        } else if (type.equalsIgnoreCase("user_channel_mode")) {
            JsonNode ops = event.ops;
            if (ops != null) {
                CollapsedEvent e = findEvent(event.nick, event.chan);
                if (e == null) {
                    e = new CollapsedEvent();
                    e.type = TYPE_MODE;
                    e.hostmask = event.hostmask;
                    e.target_mode = event.target_mode;
                    e.nick = event.nick;
                    e.chan = event.chan;
                    e.display_name = event.nick;
                }
                JsonNode add = ops.get("add");
                for (int i = 0; i < add.size(); i++) {
                    JsonNode op = add.get(i);
                    if (!e.addMode(op.get("mode").asText()))
                        return false;
                    if (e.type == TYPE_MODE) {
                        if (event.from != null && event.from.length() > 0) {
                            e.from_nick = event.from;
                            e.from_mode = event.from_mode;
                        } else {
                            e.from_nick = event.server;
                            e.from_mode = "__the_server__";
                        }
                    } else {
                        e.from_mode = event.target_mode;
                    }
                }
                JsonNode remove = ops.get("remove");
                for (int i = 0; i < remove.size(); i++) {
                    JsonNode op = remove.get(i);
                    if (!e.removeMode(op.get("mode").asText()))
                        return false;
                    if (e.type == TYPE_MODE) {
                        if (event.from != null && event.from.length() > 0) {
                            e.from_nick = event.from;
                            e.from_mode = event.from_mode;
                        } else {
                            e.from_nick = event.server;
                            e.from_mode = "__the_server__";
                        }
                    } else {
                        e.from_mode = event.target_mode;
                    }
                }
                if (!data.contains(e))
                    data.add(e);
            }
        }
        return true;
    }

    public void addEvent(long eid, int type, String nick, String old_nick, String hostmask, String from_mode, String msg, String chan, String display_name) {
        addEvent(eid, type, nick, old_nick, hostmask, from_mode, msg, null, chan, display_name);
    }

    public void addEvent(long eid, int type, String nick, String old_nick, String hostmask, String from_mode, String msg, String target_mode, String chan, String display_name) {
        CollapsedEvent e = null;

        if (type < TYPE_NICKCHANGE) {
            if (showChan) {
                if (type == TYPE_QUIT) {
                    boolean found = false;
                    for (CollapsedEvent ev : data) {
                        if (ev.type == TYPE_JOIN) {
                            ev.type = TYPE_POPIN;
                            found = true;
                        }
                    }
                    if (found)
                        return;
                } else if (type == TYPE_JOIN) {
                    for (CollapsedEvent ev : data) {
                        if (ev.type == TYPE_QUIT) {
                            ev.type = TYPE_POPOUT;
                            ev.chan = chan;
                            return;
                        } else if(ev.type == TYPE_POPOUT) {
                            type = TYPE_POPOUT;
                        }
                    }
                }
            }

            if (old_nick != null && type != TYPE_MODE) {
                e = findEvent(old_nick, chan);
                if (e != null) {
                    e.nick = nick;
                    e.display_name = display_name;
                }
            }

            if (e == null)
                e = findEvent(nick, chan);

            if (e == null) {
                e = new CollapsedEvent();
                e.eid = eid;
                e.type = type;
                e.nick = nick;
                e.old_nick = old_nick;
                e.hostmask = hostmask;
                e.from_mode = from_mode;
                e.msg = msg;
                e.target_mode = target_mode;
                e.chan = chan;
                e.display_name = display_name;
                data.add(e);
            } else {
                e.eid = eid;
                e.display_name = display_name;
                if (e.type == TYPE_MODE) {
                    e.type = type;
                    e.msg = msg;
                    e.old_nick = old_nick;
                    e.hostmask = hostmask;
                    if (from_mode != null)
                        e.from_mode = from_mode;
                    if (target_mode != null)
                        e.target_mode = target_mode;
                } else if (type != e.type && e.type == TYPE_NICKCHANGE) {
                    e.type = type;
                    e.from_mode = from_mode;
                    e.hostmask = hostmask;
                    e.nick = nick;
                    e.msg = msg;
                } else if (type == TYPE_MODE) {
                    e.from_mode = target_mode;
                } else if (type == TYPE_JOIN) {
                    if (e.type == TYPE_POPIN)
                        e.type = TYPE_JOIN;
                    else
                        e.type = TYPE_POPOUT;
                    e.from_mode = from_mode;
                    e.chan = chan;
                } else if (e.type == TYPE_POPOUT) {
                    e.type = type;
                } else if (e.type != type) {
                    e.type = TYPE_POPIN;
                }
            }
        } else {
            if (type == TYPE_NICKCHANGE) {
                for (CollapsedEvent e1 : data) {
                    if (e1.type == TYPE_NICKCHANGE && e1.nick.equalsIgnoreCase(old_nick)) {
                        if (e1.old_nick.equalsIgnoreCase(nick))
                            data.remove(e1);
                        else
                            e1.nick = nick;
                        return;
                    }
                    if ((e1.type == TYPE_JOIN || e1.type == TYPE_POPOUT) && e1.nick.equalsIgnoreCase(old_nick)) {
                        e1.old_nick = old_nick;
                        e1.nick = nick;
                        for (CollapsedEvent e2 : data) {
                            if ((e2.type == TYPE_QUIT || e2.type == TYPE_PART) && e2.nick.equalsIgnoreCase(nick)) {
                                e1.type = TYPE_POPOUT;
                                data.remove(e2);
                                break;
                            }
                        }
                        if (data.size() > 0)
                            return;
                    }
                    if ((e1.type == TYPE_QUIT || e1.type == TYPE_PART) && e1.nick.equalsIgnoreCase(nick)) {
                        e1.type = TYPE_POPOUT;
                        for (CollapsedEvent e2 : data) {
                            if (e2.type == TYPE_JOIN && e2.nick.equalsIgnoreCase(old_nick)) {
                                data.remove(e2);
                                break;
                            }
                        }
                        if (data.size() > 0)
                            return;
                    }
                }
                e = new CollapsedEvent();
                e.eid = eid;
                e.type = type;
                e.nick = nick;
                e.from_mode = from_mode;
                e.old_nick = old_nick;
                e.hostmask = hostmask;
                e.msg = msg;
                e.chan = chan;
                data.add(e);
            } else if (type == TYPE_CONNECTIONSTATUS) {
                for (CollapsedEvent e1 : data) {
                    if (e1.msg.equals(msg)) {
                        e1.count++;
                        return;
                    }
                }
                e = new CollapsedEvent();
                e.eid = eid;
                e.type = type;
                e.msg = msg;
                e.count = 1;
                data.add(e);
            } else {
                e = new CollapsedEvent();
                e.eid = eid;
                e.type = type;
                e.nick = nick;
                e.from_mode = from_mode;
                e.old_nick = old_nick;
                e.hostmask = hostmask;
                e.msg = msg;
                e.chan = chan;
                data.add(e);
            }
        }

        if (type == TYPE_QUIT && msg != null) {
            if (msg.matches("(?:[^\\s:\\/.]+\\.)+[a-z]{2,} (?:[^\\s:\\/.]+\\.)+[a-z]{2,}")) {
                String[] parts = msg.split(" ");
                if (parts.length > 1 && !parts[0].equals(parts[1])) {
                    boolean found = false;
                    for (CollapsedEvent c : data) {
                        if (c.type == TYPE_NETSPLIT && c.msg.equalsIgnoreCase(msg))
                            found = true;
                    }
                    if (!found && data.size() > 1) {
                        CollapsedEvent c = new CollapsedEvent();
                        c.eid = eid;
                        c.type = TYPE_NETSPLIT;
                        c.msg = msg;
                        data.add(c);
                    }
                }
            }
        }
    }

    public CollapsedEvent findEvent(String nick, String chan) {
        for (CollapsedEvent e : data) {
            if (e.nick != null && (e.nick.equalsIgnoreCase(nick) && (chan == null || e.chan == null || e.chan.equalsIgnoreCase(chan))))
                return e;
        }
        return null;
    }

    public String formatNick(String nick, String display_name, String from_mode, boolean colorize) {
        return formatNick(nick, display_name, from_mode, colorize, null);
    }

    public String formatNick(String nick, String display_name, String from_mode, boolean colorize, String defaultColor) {
        if(display_name == null || display_name.length() == 0)
            display_name = nick;

        ObjectNode PREFIX = null;
        if (server != null)
            PREFIX = server.PREFIX;

        if (PREFIX == null) {
            PREFIX = new ObjectMapper().createObjectNode();
            PREFIX.put(server != null ? server.MODE_OPER : "y", "!");
            PREFIX.put(server != null ? server.MODE_OWNER : "q", "~");
            PREFIX.put(server != null ? server.MODE_ADMIN : "a", "&");
            PREFIX.put(server != null ? server.MODE_OP : "o", "@");
            PREFIX.put(server != null ? server.MODE_HALFOP : "h", "%");
            PREFIX.put(server != null ? server.MODE_VOICED : "v", "+");
        }

        String color = (colorize && nick != null && nick.length() > 0)?ColorScheme.colorForNick(nick, ColorScheme.getInstance().isDarkTheme):defaultColor;

        StringBuilder output = new StringBuilder();
        boolean showSymbol = false;
        try {
            if (NetworkConnection.getInstance().getUserInfo() != null && NetworkConnection.getInstance().getUserInfo().prefs != null)
                showSymbol = NetworkConnection.getInstance().getUserInfo().prefs.getBoolean("mode-showsymbol");
        } catch (Exception e) {
        }
        String mode = "";
        if (from_mode != null && from_mode.length() > 0) {
            if (from_mode.contains((server != null && server.MODE_OPER.length() > 0) ? server.MODE_OPER : "Y"))
                mode = (server != null && server.MODE_OPER.length() > 0) ? server.MODE_OPER : "Y";
            else if (from_mode.contains((server != null && server.MODE_OPER.length() > 0) ? server.MODE_OPER.toLowerCase() : "y"))
                mode = (server != null && server.MODE_OPER.length() > 0) ? server.MODE_OPER.toLowerCase() : "y";
            else if (from_mode.contains(server != null ? server.MODE_OWNER : "q"))
                mode = server != null ? server.MODE_OWNER : "q";
            else if (from_mode.contains(server != null ? server.MODE_ADMIN : "a"))
                mode = server != null ? server.MODE_ADMIN : "a";
            else if (from_mode.contains(server != null ? server.MODE_OP : "o"))
                mode = server != null ? server.MODE_OP : "o";
            else if (from_mode.contains(server != null ? server.MODE_HALFOP : "h"))
                mode = server != null ? server.MODE_HALFOP : "h";
            else if (from_mode.contains(server != null ? server.MODE_VOICED : "v"))
                mode = server != null ? server.MODE_VOICED : "v";
            else
                mode = from_mode.substring(0, 1);
        }
        if (mode != null && mode.length() > 0) {
            if (mode_colors.containsKey(mode.toLowerCase()))
                output.append("\u0004").append(mode_colors.get(mode.toLowerCase())).append("\u0002");
            else
                output.append("\u0002");
            if (showSymbol) {
                if (PREFIX.has(mode))
                    output.append(TextUtils.htmlEncode(PREFIX.get(mode).asText()));
                else if (PREFIX.has(mode.toLowerCase()))
                    output.append(TextUtils.htmlEncode(PREFIX.get(mode.toLowerCase()).asText()));
            } else {
                output.append("•");
            }
            output.append("\u000f\u202f");
        }

        if (color != null)
            output.append("\u0004").append(color);
        if (nick != null)
            output.append(display_name);
        if (color != null)
            output.append("\u0004");
        return output.toString();
    }

    private String was(CollapsedEvent e) {
        StringBuilder was = new StringBuilder();
        String modes = e.getModes(false);

        if (e.type != TYPE_NICKCHANGE && e.old_nick != null && e.type != TYPE_MODE)
            was.append("was ").append(e.old_nick);
        if (modes != null && modes.length() > 0) {
            if (was.length() > 0)
                was.append("; ");
            was.append(modes);
        }

        if (was.length() > 0) {
            if(collapsedNickColor != null) {
                was.insert(0, "\u0004" + collapsedNickColor);
                was.append("\u000F");
            }
            was.insert(0, " (").append(")");
        }
        return was.toString();
    }

    private void colorArrow(StringBuilder message, String arrow) {
        if(collapsedNickColor != null)
            message.append("\u0004").append(collapsedNickColor);
        message.append(arrow);
        if(collapsedNickColor != null)
            message.append("\u000F");
    }

    public String getCollapsedMessage() {
        return getCollapsedMessage(false);
    }

    public String getCollapsedMessage(boolean contentDescription) {
        if(ColorScheme.getInstance().theme != null) {
            collapsedNickColor = Integer.toHexString(ColorScheme.getInstance().collapsedRowNickColor).substring(2);
        }
        StringBuilder message = new StringBuilder();

        if (data.size() == 0)
            return null;

        if (data.size() == 1 && data.get(0).modeCount() < ((data.get(0).type == TYPE_MODE) ? 2 : 1)) {
            CollapsedEvent e = data.get(0);
            switch (e.type) {
                case TYPE_NETSPLIT:
                    if(!contentDescription)
                        message.append(e.msg.replace(" ", " ↮ "));
                    break;
                case TYPE_MODE:
                    message.append(formatNick(e.nick, e.display_name, e.target_mode, false, collapsedNickColor));
                    if(collapsedNickColor != null)
                        message.append("\u0004").append(Integer.toHexString(ColorScheme.getInstance().messageTextColor).substring(2));
                    message.append(" was ").append(e.getModes(true));
                    if (e.from_nick != null) {
                        if (e.from_mode != null && e.from_mode.equalsIgnoreCase("__the_server__")) {
                            message.append(" by the server ");
                            colorArrow(message, e.from_nick);
                        } else {
                            message.append(" by ").append(formatNick(e.from_nick, null, e.from_mode, false, collapsedNickColor));
                        }
                    }
                    break;
                case TYPE_JOIN:
                    if(!contentDescription)
                        colorArrow(message, "→ ");
                    message.append(formatNick(e.nick, e.display_name, e.from_mode, false, collapsedNickColor)).append(was(e));
                    message.append(" joined");
                    if (showChan)
                        message.append(" ").append(e.chan);

                    if(!contentDescription) {
                        if (server == null || !server.isSlack())
                            message.append(" (").append(e.hostmask).append(")");
                    }
                    break;
                case TYPE_PART:
                    if(!contentDescription)
                        colorArrow(message, "← ");
                    message.append(formatNick(e.nick, e.display_name, e.from_mode, false, collapsedNickColor)).append(was(e));
                    message.append(" left");
                    if (showChan)
                        message.append(" ").append(e.chan);
                    if(!contentDescription) {
                        if (server == null || !server.isSlack())
                            message.append(" (").append(e.hostmask).append(")");
                    }
                    if (e.msg != null && e.msg.length() > 0)
                        message.append(": ").append(noColor ? ColorFormatter.strip_colors(e.msg) : e.msg);
                    break;
                case TYPE_QUIT:
                    if(!contentDescription)
                        colorArrow(message, "⇐ ");
                    message.append(formatNick(e.nick, e.display_name, e.from_mode, false, collapsedNickColor)).append(was(e));
                    if (!contentDescription && e.hostmask != null && (server == null || !server.isSlack())) {
                        message.append(" quit (").append(e.hostmask).append(") ");
                    } else {
                        message.append(" quit");
                        if(e.msg != null && e.msg.length() > 0)
                            message.append(": ");
                    }
                    if (e.msg != null && e.msg.length() > 0)
                        message.append(noColor ? ColorFormatter.strip_colors(e.msg) : e.msg);
                    break;
                case TYPE_NICKCHANGE:
                    message.append(e.old_nick);
                    if(contentDescription)
                        message.append(" changed nickname to ");
                    else
                        colorArrow(message, " → ");
                    message.append(formatNick(e.nick, e.display_name, e.from_mode, false, collapsedNickColor));
                    break;
                case TYPE_POPIN:
                    if(!contentDescription)
                        colorArrow(message, "↔ ");
                    message.append(formatNick(e.nick, e.display_name, e.from_mode, false, collapsedNickColor)).append(was(e));
                    message.append(" popped in");
                    if (showChan)
                        message.append(" ").append(e.chan);
                    break;
                case TYPE_POPOUT:
                    if(!contentDescription)
                        colorArrow(message, "↔ ");
                    message.append(formatNick(e.nick, e.display_name, e.from_mode, false, collapsedNickColor)).append(was(e));
                    message.append(" nipped out");
                    if (showChan)
                        message.append(" ").append(e.chan);
                    break;
                case TYPE_CONNECTIONSTATUS:
                    message.append(e.msg);
                    if (e.count > 1)
                        message.append(" (").append(e.count).append("x)");
                    break;
            }
        } else {
            Collections.sort(data, new comparator());
            Iterator<CollapsedEvent> i = data.iterator();
            CollapsedEvent last = null;
            CollapsedEvent next = i.next();
            CollapsedEvent e;
            int groupcount = 0;

            while (next != null) {
                e = next;

                if (i.hasNext())
                    next = i.next();
                else
                    next = null;

                if (message.length() > 0 && e.type < TYPE_NICKCHANGE && ((next == null || next.type != e.type) && last != null && last.type == e.type)) {
                    if (groupcount == 1)
                        message.delete(message.length() - 2, message.length()).append(" ");
                    message.append("and ");
                }

                if (last == null || last.type != e.type) {
                    switch (e.type) {
                        case TYPE_MODE:
                            if (message.length() > 0)
                                message.append("• ");
                            colorArrow(message, "mode: ");
                            break;
                        case TYPE_JOIN:
                            if(!contentDescription)
                                colorArrow(message, "→ ");
                            break;
                        case TYPE_PART:
                            if(!contentDescription)
                                colorArrow(message, "← ");
                            break;
                        case TYPE_QUIT:
                            if(!contentDescription)
                                colorArrow(message, "⇐ ");
                            break;
                        case TYPE_NICKCHANGE:
                            if (message.length() > 0)
                                message.append("• ");
                            break;
                        case TYPE_POPIN:
                        case TYPE_POPOUT:
                            if(!contentDescription)
                                colorArrow(message, "↔ ");
                            break;
                        case TYPE_CONNECTIONSTATUS:
                            break;
                    }
                }

                if (e.type == TYPE_NICKCHANGE) {
                    message.append(e.old_nick);
                    if(contentDescription)
                        message.append(" changed nickname to ");
                    else
                        colorArrow(message, " → ");
                    message.append(formatNick(e.nick, e.display_name, e.from_mode, false, collapsedNickColor));
                    message.append(was(e));
                } else if (e.type == TYPE_NETSPLIT) {
                    if(!contentDescription)
                        message.append(e.msg.replace(" ", " ↮ "));
                } else if (e.type == TYPE_CONNECTIONSTATUS) {
                    message.append(e.msg);
                    if (e.count > 1)
                        message.append(" (").append(e.count).append("x)");
                } else if (!showChan) {
                    message.append(formatNick(e.nick, e.display_name, (e.type == TYPE_MODE) ? e.target_mode : e.from_mode, false, collapsedNickColor)).append(was(e));
                }

                if ((next == null || next.type != e.type) && !showChan) {
                    switch (e.type) {
                        case TYPE_JOIN:
                            message.append(" joined");
                            break;
                        case TYPE_PART:
                            message.append(" left");
                            break;
                        case TYPE_QUIT:
                            message.append(" quit");
                            break;
                        case TYPE_POPIN:
                            message.append(" popped in");
                            break;
                        case TYPE_POPOUT:
                            message.append(" nipped out");
                            break;
                    }
                } else if (showChan && e.type != TYPE_NETSPLIT && e.type != TYPE_CONNECTIONSTATUS && e.type != TYPE_NICKCHANGE) {
                    if (groupcount == 0) {
                        message.append(formatNick(e.nick, e.display_name, (e.type == TYPE_MODE) ? e.target_mode : e.from_mode, false, collapsedNickColor)).append(was(e));
                        switch (e.type) {
                            case TYPE_JOIN:
                                message.append(" joined ");
                                break;
                            case TYPE_PART:
                                message.append(" left ");
                                break;
                            case TYPE_QUIT:
                                message.append(" quit ");
                                break;
                            case TYPE_POPIN:
                                message.append(" popped in ");
                                break;
                            case TYPE_POPOUT:
                                message.append(" nipped out ");
                                break;
                        }
                    }
                    if (e.type != TYPE_QUIT)
                        message.append(e.chan);
                }

                if (next != null && next.type == e.type) {
                    if (message.length() > 0) {
                        message.append(", ");
                        groupcount++;
                    }
                } else if (next != null) {
                    message.append(" ");
                    groupcount = 0;
                }

                last = e;
            }
        }
        return message.toString();
    }

    public void clear() {
        data.clear();
    }

    public int size() {
        return data.size();
    }
} public class ColorScheme {
    private static ColorScheme instance = new ColorScheme();

    public static ColorScheme getInstance() {
        return instance;
    }

    public static String getUserTheme() {
        String theme = PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getString("theme", ColorScheme.defaultTheme());
        if(theme.equals("auto"))
            return getSystemDarkMode() ? "midnight" : "dawn";
        return theme;
    }

    public static int getIRCColor(int color, boolean background) {
        String s = ColorFormatter.COLOR_MAP[color];

        if(getInstance().isDarkTheme && !background) {
            if(ColorFormatter.DARK_FG_SUBSTITUTIONS.containsKey(s))
                s = ColorFormatter.DARK_FG_SUBSTITUTIONS.get(s);
        }

        return 0xff000000 + Integer.parseInt(s, 16);
    }

    public static int getTheme(String theme, boolean actionbar) {
        switch(theme) {
            case "dawn":
                return actionbar?R.style.dawn:R.style.dawnNoActionBar;
            case "dusk":
                return actionbar?R.style.dusk:R.style.duskNoActionBar;
            case "tropic":
                return actionbar?R.style.tropic:R.style.tropicNoActionBar;
            case "emerald":
                return actionbar?R.style.emerald:R.style.emeraldNoActionBar;
            case "sand":
                return actionbar?R.style.sand:R.style.sandNoActionBar;
            case "rust":
                return actionbar?R.style.rust:R.style.rustNoActionBar;
            case "orchid":
                return actionbar?R.style.orchid:R.style.orchidNoActionBar;
            case "ash":
                return actionbar?R.style.ash:R.style.ashNoActionBar;
            case "midnight":
                return actionbar?R.style.midnight:R.style.midnightNoActionBar;
            default:
                return getSystemDarkMode()?(actionbar?R.style.midnight:R.style.midnightNoActionBar):(actionbar?R.style.dawn:R.style.dawnNoActionBar);
        }
    }

    public static boolean getSystemDarkMode() {
        if(Build.VERSION.SDK_INT < 29) {
            PowerManager pm = (PowerManager)IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.POWER_SERVICE);
            return pm.isPowerSaveMode();
        } else {
            int currentNightMode = IRCCloudApplication.getInstance().getApplicationContext().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
            return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
        }
    }

    public static String defaultTheme() {
        return Build.VERSION.SDK_INT >= 29 ? "auto" : "dawn";
    }

    public static int getDialogTheme(String theme) {
        switch(theme) {
            case "dawn":
                return R.style.dawnDialog;
            case "dusk":
                return R.style.duskDialog;
            case "tropic":
                return R.style.tropicDialog;
            case "emerald":
                return R.style.emeraldDialog;
            case "sand":
                return R.style.sandDialog;
            case "rust":
                return R.style.rustDialog;
            case "orchid":
                return R.style.orchidDialog;
            case "ash":
                return R.style.ashDialog;
            case "midnight":
                return R.style.midnightDialog;
            default:
                return getSystemDarkMode()?R.style.midnightDialog:R.style.dawnDialog;
        }
    }

    public static int getDialogWhenLargeTheme(String theme) {
        switch(theme) {
            case "dawn":
                return R.style.dawnDialogWhenLarge;
            case "dusk":
                return R.style.duskDialogWhenLarge;
            case "tropic":
                return R.style.tropicDialogWhenLarge;
            case "emerald":
                return R.style.emeraldDialogWhenLarge;
            case "sand":
                return R.style.sandDialogWhenLarge;
            case "rust":
                return R.style.rustDialogWhenLarge;
            case "orchid":
                return R.style.orchidDialogWhenLarge;
            case "ash":
                return R.style.ashDialogWhenLarge;
            case "midnight":
                return R.style.midnightDialogWhenLarge;
            default:
                return getSystemDarkMode()?R.style.midnightDialogWhenLarge:R.style.dawnDialogWhenLarge;
        }
    }

    private static String[] light_nick_colors = {
            "b22222",
            "d2691e",
            "ff9166",
            "fa8072",
            "ff8c00",
            "228b22",
            "808000",
            "b7b05d",
            "8ebd2e",
            "2ebd2e",
            "82b482",
            "37a467",
            "57c8a1",
            "1da199",
            "579193",
            "008b8b",
            "00bfff",
            "4682b4",
            "1e90ff",
            "4169e1",
            "6a5acd",
            "7b68ee",
            "9400d3",
            "8b008b",
            "ba55d3",
            "ff00ff",
            "ff1493"
    };

    private static String dark_nick_colors[] = {
            "deb887",
            "ffd700",
            "ff9166",
            "fa8072",
            "ff8c00",
            "00ff00",
            "ffff00",
            "bdb76b",
            "9acd32",
            "32cd32",
            "8fbc8f",
            "3cb371",
            "66cdaa",
            "20b2aa",
            "40e0d0",
            "00ffff",
            "00bfff",
            "87ceeb",
            "339cff",
            "6495ed",
            "b2a9e5",
            "ff69b4",
            "da70d6",
            "ee82ee",
            "d68fff",
            "ff00ff",
            "ffb6c1"
    };

    public static String colorForNick(String nick, boolean isDarkTheme) {
        String colors[] = isDarkTheme ? dark_nick_colors : light_nick_colors;
        // Normalise a bit
        // typically ` and _ are used on the end alone
        String normalizedNick = nick.toLowerCase().replaceAll("[`_]+$", "");
        //remove |<anything> from the end
        normalizedNick = normalizedNick.replaceAll("\\|.*$", "");

        Double hash = 0.0;

        for (int i = 0; i < normalizedNick.length(); i++) {
            hash = ((int) normalizedNick.charAt(i)) + (double) ((int) (hash.longValue()) << 6) + (double) ((int) (hash.longValue()) << 16) - hash;
        }

        return colors[(int) Math.abs(hash.longValue() % colors.length)];
    }

    public void setThemeFromContext(Context ctx, String theme_name) {
        theme = theme_name;
        contentBackgroundColor = colorForAttribute(ctx, R.attr.contentBackgroundColor);
        messageTextColor = colorForAttribute(ctx, R.attr.messageTextColor);
        opersGroupColor = colorForAttribute(ctx, R.attr.opersGroupColor);
        ownersGroupColor = colorForAttribute(ctx, R.attr.ownersGroupColor);
        adminsGroupColor = colorForAttribute(ctx, R.attr.adminsGroupColor);
        opsGroupColor = colorForAttribute(ctx, R.attr.opsGroupColor);
        halfopsGroupColor = colorForAttribute(ctx, R.attr.halfopsGroupColor);
        voicedGroupColor = colorForAttribute(ctx, R.attr.voicedGroupColor);
        membersGroupColor = colorForAttribute(ctx, R.attr.membersGroupColor);
        opersHeadingColor = colorForAttribute(ctx, R.attr.opersHeadingColor);
        ownersHeadingColor = colorForAttribute(ctx, R.attr.ownersHeadingColor);
        adminsHeadingColor = colorForAttribute(ctx, R.attr.adminsHeadingColor);
        opsHeadingColor = colorForAttribute(ctx, R.attr.opsHeadingColor);
        halfopsHeadingColor = colorForAttribute(ctx, R.attr.halfopsHeadingColor);
        voicedHeadingColor = colorForAttribute(ctx, R.attr.voicedHeadingColor);
        membersHeadingColor = colorForAttribute(ctx, R.attr.membersHeadingColor);
        memberListTextColor = colorForAttribute(ctx, R.attr.memberListTextColor);
        memberListAwayTextColor = colorForAttribute(ctx, R.attr.memberListAwayTextColor);
        timestampColor = colorForAttribute(ctx, R.attr.timestampColor);
        darkBlueColor = colorForAttribute(ctx, R.attr.darkBlueColor);
        networkErrorBackgroundColor = colorForAttribute(ctx, R.attr.networkErrorBackgroundColor);
        networkErrorColor = colorForAttribute(ctx, R.attr.networkErrorColor);
        errorBackgroundColor = colorForAttribute(ctx, R.attr.errorBackgroundColor);
        statusBackgroundColor = colorForAttribute(ctx, R.attr.statusBackgroundColor);
        selfBackgroundColor = colorForAttribute(ctx, R.attr.selfBackgroundColor);
        highlightBackgroundColor = colorForAttribute(ctx, R.attr.highlightBackgroundColor);
        highlightTimestampColor = colorForAttribute(ctx, R.attr.highlightTimestampColor);
        noticeBackgroundColor = colorForAttribute(ctx, R.attr.noticeBackgroundColor);
        timestampBackgroundColor = colorForAttribute(ctx, R.attr.timestampBackgroundColor);
        newMsgsBackgroundColor = colorForAttribute(ctx, R.attr.newMsgsBackgroundColor);
        collapsedRowTextColor = colorForAttribute(ctx, R.attr.collapsedRowTextColor);
        collapsedRowNickColor = colorForAttribute(ctx, R.attr.collapsedRowNickColor);
        collapsedHeadingBackgroundColor = colorForAttribute(ctx, R.attr.collapsedHeadingBackgroundColor);
        navBarColor = colorForAttribute(ctx, R.attr.navBarColor);
        navBarHeadingColor = colorForAttribute(ctx, R.attr.navBarHeadingColor);
        navBarSubheadingColor = colorForAttribute(ctx, R.attr.navBarSubheadingColor);
        navBarBorderColor = colorForAttribute(ctx, R.attr.navBarBorderColor);
        textareaTextColor = colorForAttribute(ctx, R.attr.textareaTextColor);
        textareaBackgroundColor = colorForAttribute(ctx, R.attr.textareaBackgroundColor);
        linkColor = colorForAttribute(ctx, R.attr.linkColor);
        lightLinkColor = colorForAttribute(ctx, R.attr.lightLinkColor);
        serverBackgroundColor = colorForAttribute(ctx, R.attr.serverBackgroundColor);
        bufferBackgroundColor = colorForAttribute(ctx, R.attr.bufferBackgroundColor);
        unreadBorderColor = colorForAttribute(ctx, R.attr.unreadBorderColor);
        highlightBorderColor = colorForAttribute(ctx, R.attr.highlightBorderColor);
        networkErrorBorderColor = colorForAttribute(ctx, R.attr.networkErrorBorderColor);
        bufferTextColor = colorForAttribute(ctx, R.attr.bufferTextColor);
        inactiveBufferTextColor = colorForAttribute(ctx, R.attr.inactiveBufferTextColor);
        unreadBufferTextColor = colorForAttribute(ctx, R.attr.unreadBufferTextColor);
        selectedBufferTextColor = colorForAttribute(ctx, R.attr.selectedBufferTextColor);
        selectedBufferBackgroundColor = colorForAttribute(ctx, R.attr.selectedBufferBackgroundColor);
        bufferBorderColor = colorForAttribute(ctx, R.attr.bufferBorderColor);
        selectedBufferBorderColor = colorForAttribute(ctx, R.attr.selectedBufferBorderColor);
        backlogDividerColor = colorForAttribute(ctx, R.attr.backlogDividerColor);
        chatterBarTextColor = colorForAttribute(ctx, R.attr.chatterBarTextColor);
        chatterBarColor = colorForAttribute(ctx, R.attr.chatterBarColor);
        awayBarTextColor = colorForAttribute(ctx, R.attr.awayBarTextColor);
        awayBarColor = colorForAttribute(ctx, R.attr.awayBarColor);
        connectionBarTextColor = colorForAttribute(ctx, R.attr.connectionBarTextColor);
        connectionBarColor = colorForAttribute(ctx, R.attr.connectionBarColor);
        placeholderColor = colorForAttribute(ctx, R.attr.placeholderColor);
        unreadBlueColor = colorForAttribute(ctx, R.attr.unreadBlueColor);
        serverBorderColor = colorForAttribute(ctx, R.attr.serverBorderColor);
        failedServerBorderColor = colorForAttribute(ctx, R.attr.failedServerBorderColor);
        archivesHeadingTextColor = colorForAttribute(ctx, R.attr.archivesHeadingTextColor);
        archivedChannelTextColor = colorForAttribute(ctx, R.attr.archivedChannelTextColor);
        archivedBufferTextColor = colorForAttribute(ctx, R.attr.archivedBufferTextColor);
        selectedArchivesHeadingColor = colorForAttribute(ctx, R.attr.selectedArchivesHeadingColor);
        timestampTopBorderColor = colorForAttribute(ctx, R.attr.timestampTopBorderColor);
        timestampBottomBorderColor = colorForAttribute(ctx, R.attr.timestampBottomBorderColor);
        expandCollapseIndicatorColor = colorForAttribute(ctx, R.attr.expandCollapseIndicatorColor);
        bufferHighlightColor = colorForAttribute(ctx, R.attr.bufferHighlightColor);
        selectedBufferHighlightColor = colorForAttribute(ctx, R.attr.selectedBufferHighlightColor);
        archivedBufferHighlightColor = colorForAttribute(ctx, R.attr.archivedBufferHighlightColor);
        selectedArchivedBufferHighlightColor = colorForAttribute(ctx, R.attr.selectedArchivedBufferHighlightColor);
        selectedArchivedBufferBackgroundColor = colorForAttribute(ctx, R.attr.selectedArchivedBufferBackgroundColor);
        contentBorderColor = colorForAttribute(ctx, R.attr.contentBorderColor);
        bufferBorderDrawable = resourceForAttribute(ctx, R.attr.bufferBorderDrawable);
        serverBorderDrawable = resourceForAttribute(ctx, R.attr.serverBorderDrawable);
        selectedBorderDrawable = resourceForAttribute(ctx, R.attr.selectedBorderDrawable);
        collapsedBorderDrawable = resourceForAttribute(ctx, R.attr.collapsedBorderDrawable);
        bufferBackgroundDrawable = resourceForAttribute(ctx, R.attr.bufferBackgroundDrawable);
        serverBackgroundDrawable = resourceForAttribute(ctx, R.attr.serverBackgroundDrawable);
        selectedBackgroundDrawable = resourceForAttribute(ctx, R.attr.selectedBackgroundDrawable);
        lastSeenEIDBackgroundDrawable = resourceForAttribute(ctx, R.attr.lastSeenEIDBackgroundDrawable);
        socketclosedBackgroundDrawable = resourceForAttribute(ctx, R.attr.socketclosedBackgroundDrawable);
        timestampBackgroundDrawable = resourceForAttribute(ctx, R.attr.timestampBackgroundDrawable);
        actionBarDrawable = resourceForAttribute(ctx, R.attr.actionbarDrawable);
        colorControlNormal = colorForAttribute(ctx, R.attr.colorControlNormal);
        dialogBackgroundColor = colorForAttribute(ctx, R.attr.dialogBackgroundColor);
        windowBackgroundDrawable = resourceForAttribute(ctx, R.attr.windowBackgroundDrawable);
        row_opers_bg_drawable = resourceForAttribute(ctx, R.attr.row_opers_bg_drawable);
        row_owners_bg_drawable = resourceForAttribute(ctx, R.attr.row_owners_bg_drawable);
        row_admins_bg_drawable = resourceForAttribute(ctx, R.attr.row_admins_bg_drawable);
        row_ops_bg_drawable = resourceForAttribute(ctx, R.attr.row_ops_bg_drawable);
        row_halfops_bg_drawable = resourceForAttribute(ctx, R.attr.row_halfops_bg_drawable);
        row_voiced_bg_drawable = resourceForAttribute(ctx, R.attr.row_voiced_bg_drawable);
        row_members_bg_drawable = resourceForAttribute(ctx, R.attr.row_members_bg_drawable);
        codeSpanForegroundColor = colorForAttribute(ctx, R.attr.codeSpanForegroundColor);
        codeSpanBackgroundColor = colorForAttribute(ctx, R.attr.codeSpanBackgroundColor);
        statusBarColor = colorForAttribute(ctx, android.R.attr.statusBarColor);
        isDarkTheme = !theme.equals("dawn");
        selfTextColor = isDarkTheme?"ffffff":"142b43";
    }

    private int colorForAttribute(Context ctx, int attribute) {
        TypedValue v = new TypedValue();
        ctx.getTheme().resolveAttribute(attribute, v, true);
        return v.data;
    }

    private int resourceForAttribute(Context ctx, int attribute) {
        TypedValue v = new TypedValue();
        ctx.getTheme().resolveAttribute(attribute, v, true);
        return v.resourceId;
    }

    public int contentBackgroundColor;
    public int messageTextColor;
    public int opersGroupColor;
    public int ownersGroupColor;
    public int adminsGroupColor;
    public int opsGroupColor;
    public int halfopsGroupColor;
    public int voicedGroupColor;
    public int membersGroupColor;
    public int opersHeadingColor;
    public int ownersHeadingColor;
    public int adminsHeadingColor;
    public int opsHeadingColor;
    public int halfopsHeadingColor;
    public int voicedHeadingColor;
    public int membersHeadingColor;
    public int memberListTextColor;
    public int memberListAwayTextColor;
    public int timestampColor;
    public int darkBlueColor;
    public int networkErrorBackgroundColor;
    public int networkErrorColor;
    public int errorBackgroundColor;
    public int statusBackgroundColor;
    public int selfBackgroundColor;
    public int highlightBackgroundColor;
    public int highlightTimestampColor;
    public int noticeBackgroundColor;
    public int timestampBackgroundColor;
    public int newMsgsBackgroundColor;
    public int collapsedRowTextColor;
    public int collapsedRowNickColor;
    public int collapsedHeadingBackgroundColor;
    public int navBarColor;
    public int navBarHeadingColor;
    public int navBarSubheadingColor;
    public int navBarBorderColor;
    public int textareaTextColor;
    public int textareaBackgroundColor;
    public int linkColor;
    public int lightLinkColor;
    public int serverBackgroundColor;
    public int bufferBackgroundColor;
    public int unreadBorderColor;
    public int highlightBorderColor;
    public int networkErrorBorderColor;
    public int bufferTextColor;
    public int inactiveBufferTextColor;
    public int unreadBufferTextColor;
    public int selectedBufferTextColor;
    public int selectedBufferBackgroundColor;
    public int bufferBorderColor;
    public int selectedBufferBorderColor;
    public int backlogDividerColor;
    public int chatterBarTextColor;
    public int chatterBarColor;
    public int awayBarTextColor;
    public int awayBarColor;
    public int connectionBarTextColor;
    public int connectionBarColor;
    public int placeholderColor;
    public int unreadBlueColor;
    public int serverBorderColor;
    public int failedServerBorderColor;
    public int archivesHeadingTextColor;
    public int archivedChannelTextColor;
    public int archivedBufferTextColor;
    public int selectedArchivesHeadingColor;
    public int timestampTopBorderColor;
    public int timestampBottomBorderColor;
    public int expandCollapseIndicatorColor;
    public int bufferHighlightColor;
    public int selectedBufferHighlightColor;
    public int archivedBufferHighlightColor;
    public int selectedArchivedBufferHighlightColor;
    public int selectedArchivedBufferBackgroundColor;
    public int contentBorderColor;
    public int bufferBorderDrawable;
    public int serverBorderDrawable;
    public int selectedBorderDrawable;
    public int collapsedBorderDrawable;
    public int bufferBackgroundDrawable;
    public int serverBackgroundDrawable;
    public int selectedBackgroundDrawable;
    public int lastSeenEIDBackgroundDrawable;
    public int socketclosedBackgroundDrawable;
    public int timestampBackgroundDrawable;
    public int colorControlNormal;
    public int actionBarDrawable;
    public int dialogBackgroundColor;
    public int windowBackgroundDrawable;
    public int statusBarColor;
    public int row_opers_bg_drawable;
    public int row_owners_bg_drawable;
    public int row_admins_bg_drawable;
    public int row_ops_bg_drawable;
    public int row_halfops_bg_drawable;
    public int row_voiced_bg_drawable;
    public int row_members_bg_drawable;
    public String theme;
    public boolean isDarkTheme;
    public String selfTextColor;
    public int codeSpanForegroundColor;
    public int codeSpanBackgroundColor;
} public class FontAwesome {
    public static final String FIVE_HUNDRED_PX = "\uf26E";
    public static final String ADJUST = "\uf042";
    public static final String ADN = "\uf170";
    public static final String ALIGN_CENTER = "\uf037";
    public static final String ALIGN_JUSTIFY = "\uf039";
    public static final String ALIGN_LEFT = "\uf036";
    public static final String ALIGN_RIGHT = "\uf038";
    public static final String AMAZON = "\uf270";
    public static final String AMBULANCE = "\uf0F9";
    public static final String ANCHOR = "\uf13D";
    public static final String ANDROID = "\uf17B";
    public static final String ANGELLIST = "\uf209";
    public static final String ANGLE_DOUBLE_DOWN = "\uf103";
    public static final String ANGLE_DOUBLE_LEFT = "\uf100";
    public static final String ANGLE_DOUBLE_RIGHT = "\uf101";
    public static final String ANGLE_DOUBLE_UP = "\uf102";
    public static final String ANGLE_DOWN = "\uf107";
    public static final String ANGLE_LEFT = "\uf104";
    public static final String ANGLE_RIGHT = "\uf105";
    public static final String ANGLE_UP = "\uf106";
    public static final String APPLE = "\uf179";
    public static final String ARCHIVE = "\uf187";
    public static final String AREA_CHART = "\uf1FE";
    public static final String ARROW_CIRCLE_DOWN = "\uf0AB";
    public static final String ARROW_CIRCLE_LEFT = "\uf0A8";
    public static final String ARROW_CIRCLE_O_DOWN = "\uf01A";
    public static final String ARROW_CIRCLE_O_LEFT = "\uf190";
    public static final String ARROW_CIRCLE_O_RIGHT = "\uf18E";
    public static final String ARROW_CIRCLE_O_UP = "\uf01B";
    public static final String ARROW_CIRCLE_RIGHT = "\uf0A9";
    public static final String ARROW_CIRCLE_UP = "\uf0AA";
    public static final String ARROW_DOWN = "\uf063";
    public static final String ARROW_LEFT = "\uf060";
    public static final String ARROW_RIGHT = "\uf061";
    public static final String ARROW_UP = "\uf062";
    public static final String ARROWS = "\uf047";
    public static final String ARROWS_ALT = "\uf0B2";
    public static final String ARROWS_H = "\uf07E";
    public static final String ARROWS_V = "\uf07D";
    public static final String ASTERISK = "\uf069";
    public static final String AT = "\uf1FA";
    public static final String AUTOMOBILE = "\uf1B9";
    public static final String BACKWARD = "\uf04A";
    public static final String BALANCE_SCALE = "\uf24E";
    public static final String BAN = "\uf05E";
    public static final String BANK = "\uf19C";
    public static final String BAR_CHART = "\uf080";
    public static final String BAR_CHART_O = "\uf080";
    public static final String BARCODE = "\uf02A";
    public static final String BARS = "\uf0C9";
    public static final String BATTERY_0 = "\uf244";
    public static final String BATTERY_1 = "\uf243";
    public static final String BATTERY_2 = "\uf242";
    public static final String BATTERY_3 = "\uf241";
    public static final String BATTERY_4 = "\uf240";
    public static final String BATTERY_EMPTY = "\uf244";
    public static final String BATTERY_FULL = "\uf240";
    public static final String BATTERY_HALF = "\uf242";
    public static final String BATTERY_QUARTER = "\uf243";
    public static final String BATTERY_THREE_QUARTERS = "\uf241";
    public static final String BED = "\uf236";
    public static final String BEER = "\uf0FC";
    public static final String BEHANCE = "\uf1B4";
    public static final String BEHANCE_SQUARE = "\uf1B5";
    public static final String BELL = "\uf0F3";
    public static final String BELL_O = "\uf0A2";
    public static final String BELL_SLASH = "\uf1F6";
    public static final String BELL_SLASH_O = "\uf1F7";
    public static final String BICYCLE = "\uf206";
    public static final String BINOCULARS = "\uf1E5";
    public static final String BIRTHDAY_CAKE = "\uf1FD";
    public static final String BITBUCKET = "\uf171";
    public static final String BITBUCKET_SQUARE = "\uf172";
    public static final String BITCOIN = "\uf15A";
    public static final String BLACK_TIE = "\uf27E";
    public static final String BOLD = "\uf032";
    public static final String BOLT = "\uf0E7";
    public static final String BOMB = "\uf1E2";
    public static final String BOOK = "\uf02D";
    public static final String BOOKMARK = "\uf02E";
    public static final String BOOKMARK_O = "\uf097";
    public static final String BRIEFCASE = "\uf0B1";
    public static final String BTC = "\uf15A";
    public static final String BUG = "\uf188";
    public static final String BUILDING = "\uf1AD";
    public static final String BUILDING_O = "\uf0F7";
    public static final String BULLHORN = "\uf0A1";
    public static final String BULLSEYE = "\uf140";
    public static final String BUS = "\uf207";
    public static final String BUYSELLADS = "\uf20D";
    public static final String CAB = "\uf1BA";
    public static final String CALCULATOR = "\uf1EC";
    public static final String CALENDAR = "\uf073";
    public static final String CALENDAR_CHECK_O = "\uf274";
    public static final String CALENDAR_MINUS_O = "\uf272";
    public static final String CALENDAR_O = "\uf133";
    public static final String CALENDAR_PLUS_O = "\uf271";
    public static final String CALENDAR_TIMES_O = "\uf273";
    public static final String CAMERA = "\uf030";
    public static final String CAMERA_RETRO = "\uf083";
    public static final String CAR = "\uf1B9";
    public static final String CARET_DOWN = "\uf0D7";
    public static final String CARET_LEFT = "\uf0D9";
    public static final String CARET_RIGHT = "\uf0DA";
    public static final String CARET_SQUARE_O_DOWN = "\uf150";
    public static final String CARET_SQUARE_O_LEFT = "\uf191";
    public static final String CARET_SQUARE_O_RIGHT = "\uf152";
    public static final String CARET_SQUARE_O_UP = "\uf151";
    public static final String CARET_UP = "\uf0D8";
    public static final String CART_ARROW_DOWN = "\uf218";
    public static final String CART_PLUS = "\uf217";
    public static final String CC = "\uf20A";
    public static final String CC_AMEX = "\uf1F3";
    public static final String CC_DINERS_CLUB = "\uf24C";
    public static final String CC_DISCOVER = "\uf1F2";
    public static final String CC_JCB = "\uf24B";
    public static final String CC_MASTERCARD = "\uf1F1";
    public static final String CC_PAYPAL = "\uf1F4";
    public static final String CC_STRIPE = "\uf1F5";
    public static final String CC_VISA = "\uf1F0";
    public static final String CERTIFICATE = "\uf0A3";
    public static final String CHAIN = "\uf0C1";
    public static final String CHAIN_BROKEN = "\uf127";
    public static final String CHECK = "\uf00C";
    public static final String CHECK_CIRCLE = "\uf058";
    public static final String CHECK_CIRCLE_O = "\uf05D";
    public static final String CHECK_SQUARE = "\uf14A";
    public static final String CHECK_SQUARE_O = "\uf046";
    public static final String CHEVRON_CIRCLE_DOWN = "\uf13A";
    public static final String CHEVRON_CIRCLE_LEFT = "\uf137";
    public static final String CHEVRON_CIRCLE_RIGHT = "\uf138";
    public static final String CHEVRON_CIRCLE_UP = "\uf139";
    public static final String CHEVRON_DOWN = "\uf078";
    public static final String CHEVRON_LEFT = "\uf053";
    public static final String CHEVRON_RIGHT = "\uf054";
    public static final String CHEVRON_UP = "\uf077";
    public static final String CHILD = "\uf1AE";
    public static final String CHROME = "\uf268";
    public static final String CIRCLE = "\uf111";
    public static final String CIRCLE_O = "\uf10C";
    public static final String CIRCLE_O_NOTCH = "\uf1CE";
    public static final String CIRCLE_THIN = "\uf1DB";
    public static final String CLIPBOARD = "\uf0EA";
    public static final String CLOCK_O = "\uf017";
    public static final String CLONE = "\uf24D";
    public static final String CLOSE = "\uf00D";
    public static final String CLOUD = "\uf0C2";
    public static final String CLOUD_DOWNLOAD = "\uf0ED";
    public static final String CLOUD_UPLOAD = "\uf0EE";
    public static final String CNY = "\uf157";
    public static final String CODE = "\uf121";
    public static final String CODE_FORK = "\uf126";
    public static final String CODEPEN = "\uf1CB";
    public static final String COFFEE = "\uf0F4";
    public static final String COG = "\uf013";
    public static final String COGS = "\uf085";
    public static final String COLUMNS = "\uf0DB";
    public static final String COMMENT = "\uf075";
    public static final String COMMENT_O = "\uf0E5";
    public static final String COMMENTING = "\uf27A";
    public static final String COMMENTING_O = "\uf27B";
    public static final String COMMENTS = "\uf086";
    public static final String COMMENTS_O = "\uf0E6";
    public static final String COMPASS = "\uf14E";
    public static final String COMPRESS = "\uf066";
    public static final String CONNECTDEVELOP = "\uf20E";
    public static final String CONTAO = "\uf26D";
    public static final String COPY = "\uf0C5";
    public static final String COPYRIGHT = "\uf1F9";
    public static final String CREATIVE_COMMONS = "\uf25E";
    public static final String CREDIT_CARD = "\uf09D";
    public static final String CROP = "\uf125";
    public static final String CROSSHAIRS = "\uf05B";
    public static final String CSS3 = "\uf13C";
    public static final String CUBE = "\uf1B2";
    public static final String CUBES = "\uf1B3";
    public static final String CUT = "\uf0C4";
    public static final String CUTLERY = "\uf0F5";
    public static final String DASHBOARD = "\uf0E4";
    public static final String DASHCUBE = "\uf210";
    public static final String DATABASE = "\uf1C0";
    public static final String DEDENT = "\uf03B";
    public static final String DELICIOUS = "\uf1A5";
    public static final String DESKTOP = "\uf108";
    public static final String DEVIANTART = "\uf1BD";
    public static final String DIAMOND = "\uf219";
    public static final String DIGG = "\uf1A6";
    public static final String DOLLAR = "\uf155";
    public static final String DOT_CIRCLE_O = "\uf192";
    public static final String DOWNLOAD = "\uf019";
    public static final String DRIBBBLE = "\uf17D";
    public static final String DROPBOX = "\uf16B";
    public static final String DRUPAL = "\uf1A9";
    public static final String EDIT = "\uf044";
    public static final String EJECT = "\uf052";
    public static final String ELLIPSIS_H = "\uf141";
    public static final String ELLIPSIS_V = "\uf142";
    public static final String EMPIRE = "\uf1D1";
    public static final String ENVELOPE = "\uf0E0";
    public static final String ENVELOPE_O = "\uf003";
    public static final String ENVELOPE_SQUARE = "\uf199";
    public static final String ERASER = "\uf12D";
    public static final String EUR = "\uf153";
    public static final String EURO = "\uf153";
    public static final String EXCHANGE = "\uf0EC";
    public static final String EXCLAMATION = "\uf12A";
    public static final String EXCLAMATION_CIRCLE = "\uf06A";
    public static final String EXCLAMATION_TRIANGLE = "\uf071";
    public static final String EXPAND = "\uf065";
    public static final String EXPEDITEDSSL = "\uf23E";
    public static final String EXTERNAL_LINK = "\uf08E";
    public static final String EXTERNAL_LINK_SQUARE = "\uf14C";
    public static final String EYE = "\uf06E";
    public static final String EYE_SLASH = "\uf070";
    public static final String EYEDROPPER = "\uf1FB";
    public static final String FACEBOOK = "\uf09A";
    public static final String FACEBOOK_F = "\uf09A";
    public static final String FACEBOOK_OFFICIAL = "\uf230";
    public static final String FACEBOOK_SQUARE = "\uf082";
    public static final String FAST_BACKWARD = "\uf049";
    public static final String FAST_FORWARD = "\uf050";
    public static final String FAX = "\uf1AC";
    public static final String FEED = "\uf09E";
    public static final String FEMALE = "\uf182";
    public static final String FIGHTER_JET = "\uf0FB";
    public static final String FILE = "\uf15B";
    public static final String FILE_ARCHIVE_O = "\uf1C6";
    public static final String FILE_AUDIO_O = "\uf1C7";
    public static final String FILE_CODE_O = "\uf1C9";
    public static final String FILE_EXCEL_O = "\uf1C3";
    public static final String FILE_IMAGE_O = "\uf1C5";
    public static final String FILE_MOVIE_O = "\uf1C8";
    public static final String FILE_O = "\uf016";
    public static final String FILE_PDF_O = "\uf1C1";
    public static final String FILE_PHOTO_O = "\uf1C5";
    public static final String FILE_PICTURE_O = "\uf1C5";
    public static final String FILE_POWERPOINT_O = "\uf1C4";
    public static final String FILE_SOUND_O = "\uf1C7";
    public static final String FILE_TEXT = "\uf15C";
    public static final String FILE_TEXT_O = "\uf0F6";
    public static final String FILE_VIDEO_O = "\uf1C8";
    public static final String FILE_WORD_O = "\uf1C2";
    public static final String FILE_ZIP_O = "\uf1C6";
    public static final String FILES_O = "\uf0C5";
    public static final String FILM = "\uf008";
    public static final String FILTER = "\uf0B0";
    public static final String FIRE = "\uf06D";
    public static final String FIRE_EXTINGUISHER = "\uf134";
    public static final String FIREFOX = "\uf269";
    public static final String FLAG = "\uf024";
    public static final String FLAG_CHECKERED = "\uf11E";
    public static final String FLAG_O = "\uf11D";
    public static final String FLASH = "\uf0E7";
    public static final String FLASK = "\uf0C3";
    public static final String FLICKR = "\uf16E";
    public static final String FLOPPY_O = "\uf0C7";
    public static final String FOLDER = "\uf07B";
    public static final String FOLDER_O = "\uf114";
    public static final String FOLDER_OPEN = "\uf07C";
    public static final String FOLDER_OPEN_O = "\uf115";
    public static final String FONT = "\uf031";
    public static final String FONTICONS = "\uf280";
    public static final String FORUMBEE = "\uf211";
    public static final String FORWARD = "\uf04E";
    public static final String FOURSQUARE = "\uf180";
    public static final String FROWN_O = "\uf119";
    public static final String FUTBOL_O = "\uf1E3";
    public static final String GAMEPAD = "\uf11B";
    public static final String GAVEL = "\uf0E3";
    public static final String GBP = "\uf154";
    public static final String GE = "\uf1D1";
    public static final String GEAR = "\uf013";
    public static final String GEARS = "\uf085";
    public static final String GENDERLESS = "\uf22D";
    public static final String GET_POCKET = "\uf265";
    public static final String GG = "\uf260";
    public static final String GG_CIRCLE = "\uf261";
    public static final String GIFT = "\uf06B";
    public static final String GIT = "\uf1D3";
    public static final String GIT_SQUARE = "\uf1D2";
    public static final String GITHUB = "\uf09B";
    public static final String GITHUB_ALT = "\uf113";
    public static final String GITHUB_SQUARE = "\uf092";
    public static final String GITTIP = "\uf184";
    public static final String GLASS = "\uf000";
    public static final String GLOBE = "\uf0AC";
    public static final String GOOGLE = "\uf1A0";
    public static final String GOOGLE_PLUS = "\uf0D5";
    public static final String GOOGLE_PLUS_SQUARE = "\uf0D4";
    public static final String GOOGLE_WALLET = "\uf1EE";
    public static final String GRADUATION_CAP = "\uf19D";
    public static final String GRATIPAY = "\uf184";
    public static final String GROUP = "\uf0C0";
    public static final String H_SQUARE = "\uf0FD";
    public static final String HACKER_NEWS = "\uf1D4";
    public static final String HAND_GRAB_O = "\uf255";
    public static final String HAND_LIZARD_O = "\uf258";
    public static final String HAND_O_DOWN = "\uf0A7";
    public static final String HAND_O_LEFT = "\uf0A5";
    public static final String HAND_O_RIGHT = "\uf0A4";
    public static final String HAND_O_UP = "\uf0A6";
    public static final String HAND_PAPER_O = "\uf256";
    public static final String HAND_PEACE_O = "\uf25B";
    public static final String HAND_POINTER_O = "\uf25A";
    public static final String HAND_ROCK_O = "\uf255";
    public static final String HAND_SCISSORS_O = "\uf257";
    public static final String HAND_SPOCK_O = "\uf259";
    public static final String HAND_STOP_O = "\uf256";
    public static final String HDD_O = "\uf0A0";
    public static final String HEADER = "\uf1DC";
    public static final String HEADPHONES = "\uf025";
    public static final String HEART = "\uf004";
    public static final String HEART_O = "\uf08A";
    public static final String HEARTBEAT = "\uf21E";
    public static final String HISTORY = "\uf1DA";
    public static final String HOME = "\uf015";
    public static final String HOSPITAL_O = "\uf0F8";
    public static final String HOTEL = "\uf236";
    public static final String HOURGLASS = "\uf254";
    public static final String HOURGLASS_1 = "\uf251";
    public static final String HOURGLASS_2 = "\uf252";
    public static final String HOURGLASS_3 = "\uf253";
    public static final String HOURGLASS_END = "\uf253";
    public static final String HOURGLASS_HALF = "\uf252";
    public static final String HOURGLASS_O = "\uf250";
    public static final String HOURGLASS_START = "\uf251";
    public static final String HOUZZ = "\uf27C";
    public static final String HTML5 = "\uf13B";
    public static final String I_CURSOR = "\uf246";
    public static final String ILS = "\uf20B";
    public static final String IMAGE = "\uf03E";
    public static final String INBOX = "\uf01C";
    public static final String INDENT = "\uf03C";
    public static final String INDUSTRY = "\uf275";
    public static final String INFO = "\uf129";
    public static final String INFO_CIRCLE = "\uf05A";
    public static final String INR = "\uf156";
    public static final String INSTAGRAM = "\uf16D";
    public static final String INSTITUTION = "\uf19C";
    public static final String INTERNET_EXPLORER = "\uf26B";
    public static final String INTERSEX = "\uf224";
    public static final String IOXHOST = "\uf208";
    public static final String ITALIC = "\uf033";
    public static final String JOOMLA = "\uf1AA";
    public static final String JPY = "\uf157";
    public static final String JSFIDDLE = "\uf1CC";
    public static final String KEY = "\uf084";
    public static final String KEYBOARD_O = "\uf11C";
    public static final String KRW = "\uf159";
    public static final String LANGUAGE = "\uf1AB";
    public static final String LAPTOP = "\uf109";
    public static final String LASTFM = "\uf202";
    public static final String LASTFM_SQUARE = "\uf203";
    public static final String LEAF = "\uf06C";
    public static final String LEANPUB = "\uf212";
    public static final String LEGAL = "\uf0E3";
    public static final String LEMON_O = "\uf094";
    public static final String LEVEL_DOWN = "\uf149";
    public static final String LEVEL_UP = "\uf148";
    public static final String LIFE_BOUY = "\uf1CD";
    public static final String LIFE_BUOY = "\uf1CD";
    public static final String LIFE_RING = "\uf1CD";
    public static final String LIFE_SAVER = "\uf1CD";
    public static final String LIGHTBULB_O = "\uf0EB";
    public static final String LINE_CHART = "\uf201";
    public static final String LINK = "\uf0C1";
    public static final String LINKEDIN = "\uf0E1";
    public static final String LINKEDIN_SQUARE = "\uf08C";
    public static final String LINUX = "\uf17C";
    public static final String LIST = "\uf03A";
    public static final String LIST_ALT = "\uf022";
    public static final String LIST_OL = "\uf0CB";
    public static final String LIST_UL = "\uf0CA";
    public static final String LOCATION_ARROW = "\uf124";
    public static final String LOCK = "\uf023";
    public static final String LONG_ARROW_DOWN = "\uf175";
    public static final String LONG_ARROW_LEFT = "\uf177";
    public static final String LONG_ARROW_RIGHT = "\uf178";
    public static final String LONG_ARROW_UP = "\uf176";
    public static final String MAGIC = "\uf0D0";
    public static final String MAGNET = "\uf076";
    public static final String MAIL_FORWARD = "\uf064";
    public static final String MAIL_REPLY = "\uf112";
    public static final String MAIL_REPLY_ALL = "\uf122";
    public static final String MALE = "\uf183";
    public static final String MAP = "\uf279";
    public static final String MAP_MARKER = "\uf041";
    public static final String MAP_O = "\uf278";
    public static final String MAP_PIN = "\uf276";
    public static final String MAP_SIGNS = "\uf277";
    public static final String MARS = "\uf222";
    public static final String MARS_DOUBLE = "\uf227";
    public static final String MARS_STROKE = "\uf229";
    public static final String MARS_STROKE_H = "\uf22B";
    public static final String MARS_STROKE_V = "\uf22A";
    public static final String MAXCDN = "\uf136";
    public static final String MEANPATH = "\uf20C";
    public static final String MEDIUM = "\uf23A";
    public static final String MEDKIT = "\uf0FA";
    public static final String MEH_O = "\uf11A";
    public static final String MERCURY = "\uf223";
    public static final String MICROPHONE = "\uf130";
    public static final String MICROPHONE_SLASH = "\uf131";
    public static final String MINUS = "\uf068";
    public static final String MINUS_CIRCLE = "\uf056";
    public static final String MINUS_SQUARE = "\uf146";
    public static final String MINUS_SQUARE_O = "\uf147";
    public static final String MOBILE = "\uf10B";
    public static final String MOBILE_PHONE = "\uf10B";
    public static final String MONEY = "\uf0D6";
    public static final String MOON_O = "\uf186";
    public static final String MORTAR_BOARD = "\uf19D";
    public static final String MOTORCYCLE = "\uf21C";
    public static final String MOUSE_POINTER = "\uf245";
    public static final String MUSIC = "\uf001";
    public static final String NAVICON = "\uf0C9";
    public static final String NEUTER = "\uf22C";
    public static final String NEWSPAPER_O = "\uf1EA";
    public static final String OBJECT_GROUP = "\uf247";
    public static final String OBJECT_UNGROUP = "\uf248";
    public static final String ODNOKLASSNIKI = "\uf263";
    public static final String ODNOKLASSNIKI_SQUARE = "\uf264";
    public static final String OPENCART = "\uf23D";
    public static final String OPENID = "\uf19B";
    public static final String OPERA = "\uf26A";
    public static final String OPTIN_MONSTER = "\uf23C";
    public static final String OUTDENT = "\uf03B";
    public static final String PAGELINES = "\uf18C";
    public static final String PAINT_BRUSH = "\uf1FC";
    public static final String PAPER_PLANE = "\uf1D8";
    public static final String PAPER_PLANE_O = "\uf1D9";
    public static final String PAPERCLIP = "\uf0C6";
    public static final String PARAGRAPH = "\uf1DD";
    public static final String PASTE = "\uf0EA";
    public static final String PAUSE = "\uf04C";
    public static final String PAW = "\uf1B0";
    public static final String PAYPAL = "\uf1ED";
    public static final String PENCIL = "\uf040";
    public static final String PENCIL_SQUARE = "\uf14B";
    public static final String PENCIL_SQUARE_O = "\uf044";
    public static final String PHONE = "\uf095";
    public static final String PHONE_SQUARE = "\uf098";
    public static final String PHOTO = "\uf03E";
    public static final String PICTURE_O = "\uf03E";
    public static final String PIE_CHART = "\uf200";
    public static final String PIED_PIPER = "\uf1A7";
    public static final String PIED_PIPER_ALT = "\uf1A8";
    public static final String PINTEREST = "\uf0D2";
    public static final String PINTEREST_P = "\uf231";
    public static final String PINTEREST_SQUARE = "\uf0D3";
    public static final String PLANE = "\uf072";
    public static final String PLAY = "\uf04B";
    public static final String PLAY_CIRCLE = "\uf144";
    public static final String PLAY_CIRCLE_O = "\uf01D";
    public static final String PLUG = "\uf1E6";
    public static final String PLUS = "\uf067";
    public static final String PLUS_CIRCLE = "\uf055";
    public static final String PLUS_SQUARE = "\uf0FE";
    public static final String PLUS_SQUARE_O = "\uf196";
    public static final String POWER_OFF = "\uf011";
    public static final String PRINT = "\uf02F";
    public static final String PUZZLE_PIECE = "\uf12E";
    public static final String QQ = "\uf1D6";
    public static final String QRCODE = "\uf029";
    public static final String QUESTION = "\uf128";
    public static final String QUESTION_CIRCLE = "\uf059";
    public static final String QUOTE_LEFT = "\uf10D";
    public static final String QUOTE_RIGHT = "\uf10E";
    public static final String RA = "\uf1D0";
    public static final String RANDOM = "\uf074";
    public static final String REBEL = "\uf1D0";
    public static final String RECYCLE = "\uf1B8";
    public static final String REDDIT = "\uf1A1";
    public static final String REDDIT_SQUARE = "\uf1A2";
    public static final String REFRESH = "\uf021";
    public static final String REGISTERED = "\uf25D";
    public static final String REMOVE = "\uf00D";
    public static final String RENREN = "\uf18B";
    public static final String REORDER = "\uf0C9";
    public static final String REPEAT = "\uf01E";
    public static final String REPLY = "\uf112";
    public static final String REPLY_ALL = "\uf122";
    public static final String RETWEET = "\uf079";
    public static final String RMB = "\uf157";
    public static final String ROAD = "\uf018";
    public static final String ROCKET = "\uf135";
    public static final String ROTATE_LEFT = "\uf0E2";
    public static final String ROTATE_RIGHT = "\uf01E";
    public static final String ROUBLE = "\uf158";
    public static final String RSS = "\uf09E";
    public static final String RSS_SQUARE = "\uf143";
    public static final String RUB = "\uf158";
    public static final String RUBLE = "\uf158";
    public static final String RUPEE = "\uf156";
    public static final String SAFARI = "\uf267";
    public static final String SAVE = "\uf0C7";
    public static final String SCISSORS = "\uf0C4";
    public static final String SEARCH = "\uf002";
    public static final String SEARCH_MINUS = "\uf010";
    public static final String SEARCH_PLUS = "\uf00E";
    public static final String SELLSY = "\uf213";
    public static final String SEND = "\uf1D8";
    public static final String SEND_O = "\uf1D9";
    public static final String SERVER = "\uf233";
    public static final String SHARE = "\uf064";
    public static final String SHARE_ALT = "\uf1E0";
    public static final String SHARE_ALT_SQUARE = "\uf1E1";
    public static final String SHARE_SQUARE = "\uf14D";
    public static final String SHARE_SQUARE_O = "\uf045";
    public static final String SHEKEL = "\uf20B";
    public static final String SHEQEL = "\uf20B";
    public static final String SHIELD = "\uf132";
    public static final String SHIP = "\uf21A";
    public static final String SHIRTSINBULK = "\uf214";
    public static final String SHOPPING_CART = "\uf07A";
    public static final String SIGN_IN = "\uf090";
    public static final String SIGN_OUT = "\uf08B";
    public static final String SIGNAL = "\uf012";
    public static final String SIMPLYBUILT = "\uf215";
    public static final String SITEMAP = "\uf0E8";
    public static final String SKYATLAS = "\uf216";
    public static final String SKYPE = "\uf17E";
    public static final String SLACK = "\uf198";
    public static final String SLIDERS = "\uf1DE";
    public static final String SLIDESHARE = "\uf1E7";
    public static final String SMILE_O = "\uf118";
    public static final String SOCCER_BALL_O = "\uf1E3";
    public static final String SORT = "\uf0DC";
    public static final String SORT_ALPHA_ASC = "\uf15D";
    public static final String SORT_ALPHA_DESC = "\uf15E";
    public static final String SORT_AMOUNT_ASC = "\uf160";
    public static final String SORT_AMOUNT_DESC = "\uf161";
    public static final String SORT_ASC = "\uf0DE";
    public static final String SORT_DESC = "\uf0DD";
    public static final String SORT_DOWN = "\uf0DD";
    public static final String SORT_NUMERIC_ASC = "\uf162";
    public static final String SORT_NUMERIC_DESC = "\uf163";
    public static final String SORT_UP = "\uf0DE";
    public static final String SOUNDCLOUD = "\uf1BE";
    public static final String SPACE_SHUTTLE = "\uf197";
    public static final String SPINNER = "\uf110";
    public static final String SPOON = "\uf1B1";
    public static final String SPOTIFY = "\uf1BC";
    public static final String SQUARE = "\uf0C8";
    public static final String SQUARE_O = "\uf096";
    public static final String STACK_EXCHANGE = "\uf18D";
    public static final String STACK_OVERFLOW = "\uf16C";
    public static final String STAR = "\uf005";
    public static final String STAR_HALF = "\uf089";
    public static final String STAR_HALF_EMPTY = "\uf123";
    public static final String STAR_HALF_FULL = "\uf123";
    public static final String STAR_HALF_O = "\uf123";
    public static final String STAR_O = "\uf006";
    public static final String STEAM = "\uf1B6";
    public static final String STEAM_SQUARE = "\uf1B7";
    public static final String STEP_BACKWARD = "\uf048";
    public static final String STEP_FORWARD = "\uf051";
    public static final String STETHOSCOPE = "\uf0F1";
    public static final String STICKY_NOTE = "\uf249";
    public static final String STICKY_NOTE_O = "\uf24A";
    public static final String STOP = "\uf04D";
    public static final String STREET_VIEW = "\uf21D";
    public static final String STRIKETHROUGH = "\uf0CC";
    public static final String STUMBLEUPON = "\uf1A4";
    public static final String STUMBLEUPON_CIRCLE = "\uf1A3";
    public static final String SUBSCRIPT = "\uf12C";
    public static final String SUBWAY = "\uf239";
    public static final String SUITCASE = "\uf0F2";
    public static final String SUN_O = "\uf185";
    public static final String SUPERSCRIPT = "\uf12B";
    public static final String SUPPORT = "\uf1CD";
    public static final String TABLE = "\uf0CE";
    public static final String TABLET = "\uf10A";
    public static final String TACHOMETER = "\uf0E4";
    public static final String TAG = "\uf02B";
    public static final String TAGS = "\uf02C";
    public static final String TASKS = "\uf0AE";
    public static final String TAXI = "\uf1BA";
    public static final String TELEVISION = "\uf26C";
    public static final String TENCENT_WEIBO = "\uf1D5";
    public static final String TERMINAL = "\uf120";
    public static final String TEXT_HEIGHT = "\uf034";
    public static final String TEXT_WIDTH = "\uf035";
    public static final String TH = "\uf00A";
    public static final String TH_LARGE = "\uf009";
    public static final String TH_LIST = "\uf00B";
    public static final String THUMB_TACK = "\uf08D";
    public static final String THUMBS_DOWN = "\uf165";
    public static final String THUMBS_O_DOWN = "\uf088";
    public static final String THUMBS_O_UP = "\uf087";
    public static final String THUMBS_UP = "\uf164";
    public static final String TICKET = "\uf145";
    public static final String TIMES = "\uf00D";
    public static final String TIMES_CIRCLE = "\uf057";
    public static final String TIMES_CIRCLE_O = "\uf05C";
    public static final String TINT = "\uf043";
    public static final String TOGGLE_DOWN = "\uf150";
    public static final String TOGGLE_LEFT = "\uf191";
    public static final String TOGGLE_OFF = "\uf204";
    public static final String TOGGLE_ON = "\uf205";
    public static final String TOGGLE_RIGHT = "\uf152";
    public static final String TOGGLE_UP = "\uf151";
    public static final String TRADEMARK = "\uf25C";
    public static final String TRAIN = "\uf238";
    public static final String TRANSGENDER = "\uf224";
    public static final String TRANSGENDER_ALT = "\uf225";
    public static final String TRASH = "\uf1F8";
    public static final String TRASH_O = "\uf014";
    public static final String TREE = "\uf1BB";
    public static final String TRELLO = "\uf181";
    public static final String TRIPADVISOR = "\uf262";
    public static final String TROPHY = "\uf091";
    public static final String TRUCK = "\uf0D1";
    public static final String TRY = "\uf195";
    public static final String TTY = "\uf1E4";
    public static final String TUMBLR = "\uf173";
    public static final String TUMBLR_SQUARE = "\uf174";
    public static final String TURKISH_LIRA = "\uf195";
    public static final String TV = "\uf26C";
    public static final String TWITCH = "\uf1E8";
    public static final String TWITTER = "\uf099";
    public static final String TWITTER_SQUARE = "\uf081";
    public static final String UMBRELLA = "\uf0E9";
    public static final String UNDERLINE = "\uf0CD";
    public static final String UNDO = "\uf0E2";
    public static final String UNIVERSITY = "\uf19C";
    public static final String UNLINK = "\uf127";
    public static final String UNLOCK = "\uf09C";
    public static final String UNLOCK_ALT = "\uf13E";
    public static final String UNSORTED = "\uf0DC";
    public static final String UPLOAD = "\uf093";
    public static final String USD = "\uf155";
    public static final String USER = "\uf007";
    public static final String USER_MD = "\uf0F0";
    public static final String USER_PLUS = "\uf234";
    public static final String USER_SECRET = "\uf21B";
    public static final String USER_TIMES = "\uf235";
    public static final String USERS = "\uf0C0";
    public static final String VENUS = "\uf221";
    public static final String VENUS_DOUBLE = "\uf226";
    public static final String VENUS_MARS = "\uf228";
    public static final String VIACOIN = "\uf237";
    public static final String VIDEO_CAMERA = "\uf03D";
    public static final String VIMEO = "\uf27D";
    public static final String VIMEO_SQUARE = "\uf194";
    public static final String VINE = "\uf1CA";
    public static final String VK = "\uf189";
    public static final String VOLUME_DOWN = "\uf027";
    public static final String VOLUME_OFF = "\uf026";
    public static final String VOLUME_UP = "\uf028";
    public static final String WARNING = "\uf071";
    public static final String WECHAT = "\uf1D7";
    public static final String WEIBO = "\uf18A";
    public static final String WEIXIN = "\uf1D7";
    public static final String WHATSAPP = "\uf232";
    public static final String WHEELCHAIR = "\uf193";
    public static final String WIFI = "\uf1EB";
    public static final String WIKIPEDIA_W = "\uf266";
    public static final String WINDOWS = "\uf17A";
    public static final String WON = "\uf159";
    public static final String WORDPRESS = "\uf19A";
    public static final String WRENCH = "\uf0AD";
    public static final String XING = "\uf168";
    public static final String XING_SQUARE = "\uf169";
    public static final String Y_COMBINATOR = "\uf23B";
    public static final String Y_COMBINATOR_SQUARE = "\uf1D4";
    public static final String YAHOO = "\uf19E";
    public static final String YC = "\uf23B";
    public static final String YC_SQUARE = "\uf1D4";
    public static final String YELP = "\uf1E9";
    public static final String YEN = "\uf157";
    public static final String YOUTUBE = "\uf167";
    public static final String YOUTUBE_PLAY = "\uf16A";
    public static final String YOUTUBE_SQUARE = "\uf166";

    private static Typeface typeFace = null;

    public static Typeface getTypeface() {
        if(typeFace == null)
            typeFace = ResourcesCompat.getFont(IRCCloudApplication.getInstance().getApplicationContext(), R.font.fontawesome);
        return typeFace;
    }
} @SuppressWarnings("unused")
public class IRCCloudApplication extends Application {
    private static IRCCloudApplication instance = null;

    public static IRCCloudApplication getInstance() {
        if (instance != null) {
            return instance;
        } else {
            return new IRCCloudApplication();
        }
    }

    private NetworkConnection conn = null;
    private TimerTask notifierSockerTimerTask = null;
    private static final Timer notifierTimer = new Timer("notifier-timer");
    private Typeface csFont;
    private Handler mFontsHandler;

    @Override
    public void onCreate() {
        instance = this;
        super.onCreate();
        try {
            ProviderInstaller.installIfNeeded(getApplicationContext());
        } catch (Exception e) {
            e.printStackTrace();
        }
        FirebaseApp.initializeApp(getApplicationContext());
        TrustKit.initializeWithNetworkSecurityConfiguration(getApplicationContext(), R.xml.network_security_config);
        try {
            FirebaseAnalytics.getInstance(this).setUserId(null);
            IRCCloudLog.CrashlyticsEnabled = true;
            IRCCloudLog.Log(Log.INFO, "IRCCloud", "Crashlytics Initialized");
        } catch (Exception e) {
            IRCCloudLog.CrashlyticsEnabled = false;
            e.printStackTrace();
        }
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());

        if(prefs.getInt("dbVersion", 0) < IRCCloudDatabase.VERSION) {
            getApplicationContext().getDatabasePath(IRCCloudDatabase.NAME + ".db").delete();
            SharedPreferences.Editor editor = prefs.edit();
            editor.putInt("dbVersion", IRCCloudDatabase.VERSION);
            editor.apply();
        }

        NetworkConnection.getInstance().registerForConnectivity();

        //Allocate all the shared objects at launch
        conn = NetworkConnection.getInstance();
        ColorFormatter.init();

        prefs = getSharedPreferences("prefs", 0);
        NetworkConnection.IRCCLOUD_HOST = prefs.getString("host", BuildConfig.HOST);
        NetworkConnection.IRCCLOUD_PATH = prefs.getString("path", "/");

        if(!NetworkConnection.IRCCLOUD_HOST.endsWith(".irccloud.com"))
            NetworkConnection.IRCCLOUD_HOST = BuildConfig.HOST;

        /*try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                if (0 != (getApplicationInfo().flags &= ApplicationInfo.FLAG_DEBUGGABLE))
                    WebView.setWebContentsDebuggingEnabled(true);
            }
        } catch (Exception e) {
        }*/

        FontRequest request = new FontRequest(
                "com.google.android.gms.fonts",
                "com.google.android.gms",
                "Dekko",
                R.array.com_google_android_gms_fonts_certs);

        FontsContractCompat.requestFont(getApplicationContext(), request, new FontsContractCompat.FontRequestCallback() {
            @Override
            public void onTypefaceRetrieved(Typeface typeface) {
                csFont = typeface;
                EventsList.getInstance().clearCaches();
                NetworkConnection.getInstance().notifyHandlers(NetworkConnection.EVENT_FONT_DOWNLOADED, null);
            }
        }, getFontsHandler());


        IRCCloudLog.Log(Log.INFO, "IRCCloud", "App Initialized");
    }

    @Override
    public void onTerminate() {
        NetworkConnection.getInstance().unregisterForConnectivity();
        NetworkConnection.getInstance().save(1);
        super.onTerminate();
    }

    public void onPause(final Activity context) {
        final ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        if (notifierSockerTimerTask != null)
            notifierSockerTimerTask.cancel();
        notifierSockerTimerTask = new TimerTask() {
            @Override
            public void run() {
                notifierSockerTimerTask = null;
                if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
                    if(am != null && am.getRunningAppProcesses() != null) {
                        for (ActivityManager.RunningAppProcessInfo info : am.getRunningAppProcesses()) {
                            if (info.processName.equals(context.getPackageName()) && info.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND)
                                return;
                        }
                    }
                }
                if(!conn.notifier && conn.getState() == NetworkConnection.STATE_CONNECTED) {
                    conn.disconnect();
                    if(ServersList.getInstance().count() < 1) {
                        IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "No servers configured, not connecting notifier socket");
                        return;
                    }
                    if(!(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && cm.isActiveNetworkMetered() && cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED)) {
                        try {
                            Thread.sleep(1000);
                            conn.notifier = true;
                            conn.connect();
                        } catch (Exception e) {
                        }
                    }
                }
            }
        };

        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && cm.isActiveNetworkMetered() && cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED) {
            android.util.Log.d("IRCCloud", "notifier timer scheduled for 5000 seconds");
            notifierTimer.schedule(notifierSockerTimerTask, 5000);
        } else {
            android.util.Log.d("IRCCloud", "notifier timer scheduled for 300000 seconds");
            notifierTimer.schedule(notifierSockerTimerTask, 300000);
        }
    }

    public void onResume(Activity context) {
        cancelNotifierTimer();
    }

    public void cancelNotifierTimer() {
        if(notifierSockerTimerTask != null) {
            android.util.Log.d("IRCCloud", "notifier timer cancelled");
            notifierSockerTimerTask.cancel();
            notifierSockerTimerTask = null;
        }
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        if (!NetworkConnection.getInstance().isVisible()) {
            IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "Received low memory warning in the background, cleaning backlog in all buffers");
            BuffersList buffersList = BuffersList.getInstance();
            EventsList eventsList = EventsList.getInstance();
            for (Buffer b : buffersList.getBuffers()) {
                if (!b.getScrolledUp())
                    eventsList.pruneEvents(b.getBid());
            }
        }
        ImageList.getInstance().clear();
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);
        if(level >= TRIM_MEMORY_MODERATE) {
            onLowMemory();
        }
    }

    public Typeface getCsFont() {
        return csFont;
    }

    private Handler getFontsHandler() {
        if (mFontsHandler == null) {
            HandlerThread handlerThread = new HandlerThread("fonts");
            handlerThread.start();
            mFontsHandler = new Handler(handlerThread.getLooper());
        }
        return mFontsHandler;
    }
} public class IRCCloudJSONObject {
    private JsonNode o;
    private int cid = -1;
    private int bid = -1;
    private long eid = -1;
    private String type = null;

    public IRCCloudJSONObject() {
        o = new ObjectMapper().createObjectNode();
    }

    public IRCCloudJSONObject(JsonNode object) {
        o = object;
    }

    public IRCCloudJSONObject(String message) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            o = mapper.readValue(message, JsonNode.class);
        } catch (IOException e) {
            NetworkConnection.printStackTraceToCrashlytics(e);
        }
    }

    public IRCCloudJSONObject(JSONObject object) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            o = mapper.readValue(object.toString(), JsonNode.class);
        } catch (IOException e) {
            NetworkConnection.printStackTraceToCrashlytics(e);
        }
    }

    public int cid() {
        if (cid == -1)
            cid = getInt("cid");
        return cid;
    }

    public int bid() {
        if (bid == -1)
            bid = getInt("bid");
        return bid;
    }

    public long eid() {
        if (eid == -1)
            eid = getLong("eid");
        return eid;
    }

    public void setEid(long e) {
        eid = e;
    }

    public String type() {
        if (type == null) {
            if (o.has("type"))
                type = o.get("type").asText();
            else
                type = "undefined";
        }
        return type;
    }

    public boolean has(String name) {
        return o.has(name) && !o.get(name).isNull();
    }

    public boolean getBoolean(String name) {
        JsonNode node = o.get(name);
        if(node != null)
            return node.asBoolean(false);
        else
            return false;
    }

    public int getInt(String name) {
        JsonNode node = o.get(name);
        if(node != null)
            return node.asInt(-1);
        else
            return -1;
    }

    public long getLong(String name) {
        JsonNode node = o.get(name);
        if(node != null)
            return node.asLong(-1);
        else
            return -1;
    }

    public String getString(String name) {
        JsonNode node = o.get(name);
        if(node == null || node.isNull())
            return null;
        else
            return node.asText();
    }

    public JsonNode getJsonNode(String name) {
        return o.get(name);
    }

    public ObjectNode getJsonObject(String name) {
        try {
            return (ObjectNode) getJsonNode(name);
        } catch (ClassCastException e) {
            return null;
        }
    }

    public JsonNode getObject() {
        return o;
    }

    public String toString() {
        ObjectMapper mapper = new ObjectMapper();
        StringWriter writer = new StringWriter();
        try {
            mapper.writeValue(writer, o);
        } catch (IOException e) {
            NetworkConnection.printStackTraceToCrashlytics(e);
        }
        return writer.toString();
    }

    public void remove(String key) {
        if(o instanceof ObjectNode) {
            ((ObjectNode)o).remove(key);
        }
    }
} public class IRCCloudLinkMovementMethod extends LinkMovementMethod {
    private static IRCCloudLinkMovementMethod instance = null;
    private static HashMap<String, String> file_ids = new HashMap<>();
    public static boolean bubble;

    private static class FetchFileIDTask extends AsyncTaskEx<String, Void, JSONObject> {
        public Uri original_url;
        public Context context;

        @Override
        protected JSONObject doInBackground(String... params) {
            if(!isCancelled()) {
                try {
                    Thread.sleep(1000);
                    return NetworkConnection.getInstance().fetchJSON("https://" + NetworkConnection.IRCCLOUD_HOST + "/file/json/" + params[0]);
                } catch (Exception e) {
                }
            }
            return null;
        }

        @Override
        protected void onPostExecute(JSONObject result) {
            if(!isCancelled()) {
                try {
                    if (result != null && result.has("mime_type")) {
                        UriTemplate template = UriTemplate.fromTemplate(NetworkConnection.file_uri_template);
                        template.set("id", result.getString("id"));
                        String mime_type = result.getString("mime_type");
                        String extension = result.has("extension") ? result.getString("extension") : null;
                        if(extension == null || extension.length() == 0)
                            extension = "." + mime_type.substring(mime_type.indexOf("/") + 1);

                        String name = result.has("name") ? result.getString("name") : "";
                        if(!name.toLowerCase().endsWith(extension.toLowerCase()))
                            name = result.getString("id") +  extension;

                        template.set("name", name);

                        if (PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("imageviewer", true)) {
                            if(ImageList.isImageURL(template.expand())) {
                                original_url = Uri.parse(IRCCloudApplication.getInstance().getApplicationContext().getResources().getString(R.string.IMAGE_SCHEME) + template.expand().substring(4));
                            }
                        }

                        if (PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("videoviewer", true)) {
                            if(mime_type.equals("video/mp4") || mime_type.equals("video/webm") || mime_type.equals("video/3gpp") || name.toLowerCase().matches("(^.*/.*\\.3gpp?)|(^.*/.*\\.mp4$)|(^.*/.*\\.m4v$)|(^.*/.*\\.webm$)")) {
                                original_url = Uri.parse(IRCCloudApplication.getInstance().getApplicationContext().getResources().getString(R.string.VIDEO_SCHEME) + template.expand().substring(4));
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

                launchBrowser(original_url, context);
            }
        }
    }


    public synchronized static IRCCloudLinkMovementMethod getInstance() {
        if (instance == null)
            instance = new IRCCloudLinkMovementMethod();
        return instance;
    }

    @Override
    protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode, int movementMetaState, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_CENTER:
            case KeyEvent.KEYCODE_ENTER:
                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
                    if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
                        Layout layout = widget.getLayout();
                        int padding = widget.getTotalPaddingTop() + widget.getTotalPaddingBottom();
                        int areaTop = widget.getScrollY();
                        int areaBot = areaTop + widget.getHeight() - padding;
                        int lineTop = layout.getLineForVertical(areaTop);
                        int lineBot = layout.getLineForVertical(areaBot);
                        int first = layout.getLineStart(lineTop);
                        int last = layout.getLineEnd(lineBot);
                        int a = Selection.getSelectionStart(buffer);
                        int b = Selection.getSelectionEnd(buffer);
                        int selStart = Math.min(a, b);
                        int selEnd = Math.max(a, b);
                        if (selStart < 0) {
                            if (buffer.getSpanStart(FROM_BELOW) >= 0) {
                                selStart = selEnd = buffer.length();
                            }
                        }
                        if (selStart > last)
                            selStart = selEnd = Integer.MAX_VALUE;
                        if (selEnd < first)
                            selStart = selEnd = -1;

                        if (selStart == selEnd) {
                            return false;
                        }
                        ClickableSpan[] links = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
                        if (links.length != 1) {
                            return false;
                        }

                        if(links[0] instanceof URLSpan)
                            launchURI(Uri.parse(((URLSpan)links[0]).getURL()), widget.getContext());
                        else
                            links[0].onClick(widget);

                        return true;
                    }
                }
                break;
        }
        return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
    }

    @Override
    public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
        int action = event.getAction();

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
            int x = (int) event.getX();
            int y = (int) event.getY();
            x -= widget.getTotalPaddingLeft();
            y -= widget.getTotalPaddingTop();
            x += widget.getScrollX();
            y += widget.getScrollY();
            Layout layout = widget.getLayout();
            int line = layout.getLineForVertical(y);
            int off = layout.getOffsetForHorizontal(line, x);
            ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);
            if (links.length != 0) {
                if (action == MotionEvent.ACTION_UP) {
                    if(links[0] instanceof URLSpan)
                        launchURI(Uri.parse(((URLSpan)links[0]).getURL()), widget.getContext());
                    else
                        links[0].onClick(widget);
                } else {
                    Selection.setSelection(buffer, buffer.getSpanStart(links[0]), buffer.getSpanEnd(links[0]));
                }
                return true;
            } else {
                Selection.removeSelection(buffer);
            }
        }
        return false;
    }

    public static void clearFileIDs() {
        file_ids.clear();
    }

    public static void addFileID(String url, String file_id) {
        file_ids.put(url, file_id);
    }

    public static void launchURI(Uri uri, Context context) {
        if(file_ids.containsKey(uri.toString())) {
            FetchFileIDTask task = new FetchFileIDTask();
            task.original_url = uri;
            task.context = context;
            task.execute(file_ids.get(uri.toString()));
            return;
        }

        if (PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("imageviewer", true) && ImageList.isImageURL(uri.toString())) {
            Intent intent = new Intent(context, ImageViewerActivity.class);
            intent.setData(uri);
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(intent);
            return;
        }
        launchBrowser(uri, context);
    }

    public static void launchBrowser(Uri uri, Context context) {
        if(!bubble && !PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("browser", false) && uri.getScheme().startsWith("http") && CustomTabsHelper.getPackageNameToUse(context) != null) {
            CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
            builder.setToolbarColor(ColorScheme.getInstance().navBarColor);
            builder.addDefaultShareMenuItem();
            builder.addMenuItem("Copy URL", PendingIntent.getBroadcast(context, 0, new Intent(context, ChromeCopyLinkBroadcastReceiver.class), Build.VERSION.SDK_INT < 23 ? PendingIntent.FLAG_UPDATE_CURRENT : (PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)));
            CustomTabsIntent intent = builder.build();
            intent.intent.setData(uri);
            try {
                if (intent.startAnimationBundle != null) {
                    context.startActivity(intent.intent, intent.startAnimationBundle);
                } else {
                    context.startActivity(intent.intent);
                }
            } catch (ActivityNotFoundException e) {
                Toast.makeText(context, "Unable to find an application to handle this URL scheme", Toast.LENGTH_SHORT).show();
            } catch (Exception e) {
                Toast.makeText(context, "An error occurred while launching this URL: " + e.getMessage(), Toast.LENGTH_SHORT).show();
            }
        } else {
            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
            if(!uri.toString().startsWith("irccloud-") || bubble)
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
            try {
                context.startActivity(intent);
            } catch (ActivityNotFoundException e) {
                Toast.makeText(context, "Unable to find an application to handle this URL scheme", Toast.LENGTH_SHORT).show();
            } catch (Exception e) {
                Toast.makeText(context, "An error occurred while launching this URL: " + e.getMessage(), Toast.LENGTH_SHORT).show();
            }
        }
    }

    // from https://stackoverflow.com/a/23268821/3221253
    public static void forwardToBrowser(Intent i, Context context) {
        Intent intent = new Intent();
        intent.setAction(android.content.Intent.ACTION_VIEW);
        intent.addCategory(Intent.CATEGORY_BROWSABLE);
        intent.setDataAndType(Uri.fromParts("http", "", null), i.getType());
        List<ResolveInfo> activities = context.getPackageManager().queryIntentActivities(intent, 0);
        ArrayList<Intent> targetIntents = new ArrayList<Intent>();
        String thisPackageName = context.getPackageName();
        for (ResolveInfo currentInfo : activities) {
            String packageName = currentInfo.activityInfo.packageName;
            if (!thisPackageName.equals(packageName)) {
                Intent targetIntent = new Intent(android.content.Intent.ACTION_VIEW);
                targetIntent.setDataAndType(i.getData(),i.getType());
                targetIntent.setPackage(packageName);
                targetIntent.setComponent(new ComponentName(packageName, currentInfo.activityInfo.name));
                targetIntents.add(targetIntent);
            }
        }
        if(targetIntents.size() > 0) {
            Intent chooserIntent = Intent.createChooser(targetIntents.remove(0), "Open with");
            chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetIntents.toArray(new Parcelable[] {}));
            context.startActivity(chooserIntent);
        } else {
            Toast.makeText(context, "Unable to find an application to handle this URL scheme", Toast.LENGTH_SHORT).show();
        }
    }

    private static Object FROM_BELOW = new NoCopySpan.Concrete();
} public class IRCCloudLog {
    private static final LinkedList<String> lines = new LinkedList<>();
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS", Locale.US);
    public static boolean CrashlyticsEnabled = false;
    public static String defaultTag = "IRCCloud";
    public static int defaultLevel = Log.INFO;

    public static void clear() {
        synchronized (lines) {
            lines.clear();
        }
    }

    public static String lines() {
        StringBuilder b = new StringBuilder();

        synchronized (lines) {
            for (String s : lines) {
                b.append(s).append("\n");
            }
        }

        return b.toString();
    }

    public static void LogException(Throwable t) {
        StringWriter sw = new StringWriter();
        t.printStackTrace(new PrintWriter(sw));
        Log(Log.ERROR, defaultTag, sw.toString());

        if(CrashlyticsEnabled)
            FirebaseCrashlytics.getInstance().recordException(t);
    }

    public static void Log(String message) {
        Log(defaultLevel, defaultTag, message);
    }

    public static void Log(int priority, String tag, String message) {
        if(BuildConfig.DEBUG || !CrashlyticsEnabled)
            android.util.Log.println(priority, tag, message);

        StringBuilder b = new StringBuilder();
        b.append(dateFormat.format(new Date())).append(" ");
        switch(priority) {
            case Log.ASSERT:
                b.append("A/");
                break;
            case Log.DEBUG:
                b.append("D/");
                break;
            case Log.ERROR:
                b.append("E/");
                break;
            case Log.INFO:
                b.append("I/");
                break;
            case Log.VERBOSE:
                b.append("V/");
                break;
            case Log.WARN:
                b.append("W/");
                break;
        }
        b.append(tag).append(": ");
        b.append(message);
        synchronized (lines) {
            lines.addLast(b.toString());
            while(lines.size() > 200)
                lines.removeFirst();
        }

        if(CrashlyticsEnabled)
            FirebaseCrashlytics.getInstance().log(b.toString());
    }
} public class IRCEditText extends RichEditText {
    private List<SpanWrapper> typingSpans = new ArrayList<>();
    private List<Effect> typingEffects = new ArrayList<>();
    int typing_fg = -1;
    int typing_bg = -1;
    private boolean selectionChangedByUser = false;
    private Runnable selectionChangeRunnable = new Runnable() {
        @Override
        public void run() {
            selectionChangedByUser = true;
            onSelectionChanged(getSelectionStart(), getSelectionEnd());
        }
    };

    public IRCEditText(Context context) {
        super(context);
        init();
    }

    public IRCEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public IRCEditText(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        addTextChangedListener(new TextWatcher() {
            private int size, oldLength, changeStart;

            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                selectionChangedByUser = false;
                oldLength = s.length();
                changeStart = start;
                //android.util.Log.d("IRCCloud", "Text will change: " + start + ", " + count + ", after: " + after);
                removeCallbacks(selectionChangeRunnable);
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                size = count - before;
                if (getSelectionStart() == getSelectionEnd()) {
                    //android.util.Log.d("IRCCloud", "change start: " + changeStart + " end: " + getSelectionEnd() + " size: " + size);
                    if(size > 0) {
                        if (typingEffects.contains(RichEditText.BOLD)) {
                            //android.util.Log.d("IRCCloud", "Typing effects contains bold");
                            boolean found = false;
                            for (SpanWrapper span : typingSpans) {
                                if (span.span instanceof StyleSpan && ((StyleSpan) span.span).getStyle() == Typeface.BOLD) {
                                    if (changeStart >= span.start && changeStart <= span.end) {
                                        if(span.start == span.end) {
                                            //android.util.Log.d("IRCCloud", "Replacing zero-length span");
                                            typingSpans.remove(span);
                                        } else {
                                            //android.util.Log.d("IRCCloud", "Expanding existing span");
                                            found = true;
                                            span.end += size;
                                            span.added = false;
                                        }
                                        break;
                                    }
                                }
                            }
                            if (!found) {
                                //android.util.Log.d("IRCCloud", "Inserting new span");
                                SpanWrapper span = new SpanWrapper();
                                span.start = changeStart;
                                span.end = span.start + size;
                                span.span = new StyleSpan(Typeface.BOLD);
                                typingSpans.add(span);
                            }
                        }
                        if (typingEffects.contains(RichEditText.ITALIC)) {
                            boolean found = false;
                            for (SpanWrapper span : typingSpans) {
                                if (span.span instanceof StyleSpan && ((StyleSpan) span.span).getStyle() == Typeface.ITALIC) {
                                    if (changeStart >= span.start && changeStart <= span.end) {
                                        if(span.start == span.end) {
                                            typingSpans.remove(span);
                                        } else {
                                            found = true;
                                            span.end += size;
                                            span.added = false;
                                        }
                                        break;
                                    }
                                }
                            }
                            if (!found) {
                                SpanWrapper span = new SpanWrapper();
                                span.start = changeStart;
                                span.end = span.start + size;
                                span.span = new StyleSpan(Typeface.ITALIC);
                                typingSpans.add(span);
                            }
                        }
                        if (typingEffects.contains(RichEditText.UNDERLINE)) {
                            boolean found = false;
                            for (SpanWrapper span : typingSpans) {
                                if (span.span instanceof UnderlineSpan) {
                                    if (changeStart >= span.start && changeStart <= span.end) {
                                        if(span.start == span.end) {
                                            typingSpans.remove(span);
                                        } else {
                                            found = true;
                                            span.end += size;
                                            span.added = false;
                                        }
                                        break;
                                    }
                                }
                            }
                            if (!found) {
                                SpanWrapper span = new SpanWrapper();
                                span.start = changeStart;
                                span.end = span.start + size;
                                span.span = new UnderlineSpan();
                                typingSpans.add(span);
                            }
                        }
                        if (typingEffects.contains(RichEditText.STRIKETHROUGH)) {
                            boolean found = false;
                            for (SpanWrapper span : typingSpans) {
                                if (span.span instanceof StrikethroughSpan) {
                                    if (changeStart >= span.start && changeStart <= span.end) {
                                        if(span.start == span.end) {
                                            typingSpans.remove(span);
                                        } else {
                                            found = true;
                                            span.end += size;
                                            span.added = false;
                                        }
                                        break;
                                    }
                                }
                            }
                            if (!found) {
                                SpanWrapper span = new SpanWrapper();
                                span.start = changeStart;
                                span.end = span.start + size;
                                span.span = new StrikethroughSpan();
                                typingSpans.add(span);
                            }
                        }
                        if (typingEffects.contains(RichEditText.FOREGROUND)) {
                            boolean found = false;
                            for (SpanWrapper span : typingSpans) {
                                if (span.span instanceof ForegroundColorSpan && ((ForegroundColorSpan)span.span).getForegroundColor() == typing_fg) {
                                    if (changeStart >= span.start && changeStart <= span.end) {
                                        if(span.start == span.end) {
                                            typingSpans.remove(span);
                                        } else {
                                            found = true;
                                            span.end += size;
                                            span.added = false;
                                        }
                                        break;
                                    }
                                }
                            }
                            if (!found) {
                                SpanWrapper span = new SpanWrapper();
                                span.start = changeStart;
                                span.end = span.start + size;
                                span.span = new ForegroundColorSpan(typing_fg);
                                typingSpans.add(span);
                            }
                        }
                        if (typingEffects.contains(RichEditText.BACKGROUND)) {
                            boolean found = false;
                            for (SpanWrapper span : typingSpans) {
                                if (span.span instanceof BackgroundColorSpan && ((BackgroundColorSpan)span.span).getBackgroundColor() == typing_bg) {
                                    if (changeStart >= span.start && changeStart <= span.end) {
                                        if(span.start == span.end) {
                                            typingSpans.remove(span);
                                        } else {
                                            found = true;
                                            span.end += size;
                                            span.added = false;
                                        }
                                        break;
                                    }
                                    break;
                                }
                            }
                            if (!found) {
                                SpanWrapper span = new SpanWrapper();
                                span.start = changeStart;
                                span.end = span.start + size;
                                span.span = new BackgroundColorSpan(typing_bg);
                                typingSpans.add(span);
                            }
                        }
                    }
                }
            }

            @Override
            public void afterTextChanged(Editable editable) {
                if(oldLength > 0 && editable.length() == 0) {
                    typingSpans.clear();
                    typingEffects.clear();
                } else {
                    ArrayList<SpanWrapper> spansToRemove = new ArrayList<>();

                    for (SpanWrapper span : typingSpans) {
                        //android.util.Log.d("IRCCloud", "Span: " + span);
                        if (size > 0) {
                            if (span.added) {
                                if (span.start >= changeStart) {
                                    //android.util.Log.d("IRCCloud", "span starts after insertion range, offsetting");
                                    span.start += size;
                                    span.end += size;
                                } else if (span.end > changeStart) {
                                    //android.util.Log.d("IRCCloud", "span ends within insertion range, expanding");
                                    span.end += size;
                                }
                            }
                        } else {
                            if (span.start > changeStart) {
                                //android.util.Log.d("IRCCloud", "span starts after deletion range, offsetting");
                                span.start += size;
                                span.end += size;
                            } else if (span.end > changeStart) {
                                //android.util.Log.d("IRCCloud", "span ends after deletion range, shrinking");
                                span.end += size;
                            }
                        }

                        if (span.end > editable.length()) {
                            //android.util.Log.d("IRCCloud", "span exceeds editable range, truncating");
                            span.end = editable.length();
                        }

                        if (span.end < span.start) {
                            //android.util.Log.d("IRCCloud", "span too small, removing");
                            editable.removeSpan(span.span);
                            spansToRemove.add(span);
                            continue;
                        }

                        if(span.start >= 0 && span.end <= editable.length()) {
                            editable.setSpan(span.span, span.start, span.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                            span.added = true;
                        }
                    }

                    if (spansToRemove.size() > 0) {
                        //android.util.Log.d("IRCCloud", "Removing spans: " + typingSpans);
                        typingSpans.removeAll(spansToRemove);
                    }
                    //android.util.Log.d("IRCCloud", "Spans: " + typingSpans);
                }
                postDelayed(selectionChangeRunnable, 100);
            }
        });
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        selectionChangedByUser = true;
        try {
            return super.onTouchEvent(event);
        } catch (Exception e) {
            //Android 6.0 bug
            return false;
        }
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        selectionChangedByUser = true;
        boolean result = super.onKeyUp(keyCode, event);
        if(!event.isCtrlPressed() && !event.isShiftPressed() && !event.isMetaPressed()) {
            if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
                lastSelectionStart = -1;
                lastSelectionEnd = -2;
                onSelectionChanged(getSelectionStart(), getSelectionEnd());
            }
        }
        return result;
    }

    @Override
    public void setText(CharSequence text, BufferType type) {
        try {
            super.setText(text, type);
        } catch (Exception e) {
            //Fix a crash on some Samsung devices
        }
        if(text == null || text.length() == 0) {
            clearTypingEffects();
            onSelectionChanged(0, 0);
        }
    }

    private void applyTypingSpans() {
        if(typingSpans != null) {
            Spannable text = getText();
            for (SpanWrapper w : typingSpans) {
                if(w.start >= 0 && w.end >= w.start && w.end <= text.length())
                    text.setSpan(w.span, w.start, w.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
    }

    public String toIRC() {
        Spannable text = getText();
        StringBuilder out = new StringBuilder();

        int next;
        for (int i = 0; i < text.length(); i = next) {
            next = text.nextSpanTransition(i, text.length(), CharacterStyle.class);
            String fgColorRGB = null, bgColorRGB = null, fgColormIRC = null, bgColormIRC = null;
            boolean hasURL = false, shouldClear = false, hasBold = false, hasItalics = false, hasUnderline = false, hasStrikethrough = false, hasMonospace = false;
            for (CharacterStyle style : text.getSpans(i, next, CharacterStyle.class)) {
                if (style instanceof URLSpan) {
                    hasURL = true;
                    fgColorRGB = bgColorRGB = null;
                }
                if (style instanceof StyleSpan) {
                    int s = ((StyleSpan) style).getStyle();
                    if ((s & Typeface.BOLD) != 0) {
                        hasBold = true;
                        shouldClear = true;
                    }
                    if ((s & Typeface.ITALIC) != 0) {
                        hasItalics = true;
                        shouldClear = true;
                    }
                }
                if (style instanceof TypefaceSpan) {
                    String s = ((TypefaceSpan) style).getFamily();
                    if ("monospace".equals(s)) {
                        hasMonospace = true;
                        shouldClear = true;
                    }
                }
                if (style instanceof UnderlineSpan) {
                    hasUnderline = true;
                    shouldClear = true;
                }
                if (style instanceof StrikethroughSpan) {
                    hasStrikethrough = true;
                    shouldClear = true;
                }
                if(!hasURL) {
                    if (style instanceof ForegroundColorSpan) {
                        fgColorRGB = String.format("%06X", 0xFFFFFF & ((ForegroundColorSpan) style).getForegroundColor());
                        for (int c = 0; c < ColorFormatter.COLOR_MAP.length; c++) {
                            String color = ColorFormatter.COLOR_MAP[c];
                            if (fgColorRGB.equalsIgnoreCase(color) || fgColorRGB.equalsIgnoreCase(ColorFormatter.DARK_FG_SUBSTITUTIONS.get(color))) {
                                fgColormIRC = String.valueOf(c);
                                if (fgColormIRC.length() == 1)
                                    fgColormIRC = "0" + fgColormIRC;
                                break;
                            }
                        }
                    }
                    if (style instanceof BackgroundColorSpan) {
                        bgColorRGB = String.format("%06X", 0xFFFFFF & ((BackgroundColorSpan) style).getBackgroundColor());
                        for (int c = 0; c < ColorFormatter.COLOR_MAP.length; c++) {
                            String color = ColorFormatter.COLOR_MAP[c];
                            if (bgColorRGB.equalsIgnoreCase(color)) {
                                bgColormIRC = String.valueOf(c);
                                if (bgColormIRC.length() == 1)
                                    bgColormIRC = "0" + bgColormIRC;
                                break;
                            }
                        }
                    }
                }
            }
            if(hasBold)
                out.append((char)0x02);

            if(hasItalics)
                out.append((char)0x1d);

            if(hasUnderline)
                out.append((char)0x1f);

            if(hasMonospace)
                out.append((char)0x11);

            if(hasStrikethrough)
                out.append((char)0x1e);

            if(fgColorRGB != null || bgColorRGB != null) {
                if((fgColormIRC != null && (bgColorRGB == null || bgColormIRC != null)) || (fgColormIRC == null && bgColormIRC != null)) {
                    out.append((char) 0x03);
                    if (fgColormIRC != null)
                        out.append(fgColormIRC);
                    if (bgColormIRC != null)
                        out.append(",").append(bgColormIRC);
                } else {
                    out.append((char) 0x04);
                    if (fgColorRGB != null)
                        out.append(fgColorRGB);
                    if (bgColorRGB != null)
                        out.append(",").append(bgColorRGB);
                }
                shouldClear = true;
            }
            out.append(text.subSequence(i, next));
            if(shouldClear)
                out.append("\u000f");
        }

        return out.toString();
    }

    private class SpanWrapper {
        public int start;
        public int end;
        public Object span;
        public boolean added;

        @Override
        public String toString() {
            return "{ start: " + start + ", end: " + end + ", span: " + span + " }";
        }
    }

    @Override
    public boolean hasEffect(Effect effect) {
        return typingEffects.contains(effect);
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T getEffectValue(Effect<T> effect) {
        if(getSelectionStart() == getSelectionEnd() || effect.valueInSelection(this) == null) {
            if(effect == RichEditText.FOREGROUND)
                return (T)Integer.valueOf(typing_fg);
            else if(effect == RichEditText.BACKGROUND)
                return (T)Integer.valueOf(typing_bg);
            else
                return (T)Boolean.valueOf(hasEffect(effect));
        } else {
            return (effect.valueInSelection(this));
        }
    }

    private boolean shouldRemoveSpan(CharacterStyle style) {
        if (style instanceof StyleSpan && ((StyleSpan) style).getStyle() == Typeface.BOLD)
            return !typingEffects.contains(RichEditText.BOLD);
        if (style instanceof StyleSpan && ((StyleSpan) style).getStyle() == Typeface.ITALIC)
            return !typingEffects.contains(RichEditText.ITALIC);
        if (style instanceof UnderlineSpan)
            return !typingEffects.contains(RichEditText.UNDERLINE);
        if (style instanceof ForegroundColorSpan)
            return ((ForegroundColorSpan)style).getForegroundColor() != typing_fg;
        if (style instanceof BackgroundColorSpan)
            return ((BackgroundColorSpan)style).getBackgroundColor() != typing_bg;
        return false;
    }

    private int lastSelectionStart, lastSelectionEnd;

    private boolean compare(CharacterStyle s1, CharacterStyle s2) {
        if(s1 instanceof StyleSpan && s2 instanceof StyleSpan)
            return ((StyleSpan)s1).getStyle() == ((StyleSpan)s2).getStyle();
        if(s1 instanceof ForegroundColorSpan && s2 instanceof ForegroundColorSpan)
            return ((ForegroundColorSpan)s1).getForegroundColor() == ((ForegroundColorSpan)s2).getForegroundColor();
        if(s1 instanceof BackgroundColorSpan && s2 instanceof BackgroundColorSpan)
            return ((BackgroundColorSpan)s1).getBackgroundColor() == ((BackgroundColorSpan)s2).getBackgroundColor();
        if(s1 instanceof UnderlineSpan && s2 instanceof UnderlineSpan)
            return true;
        if(s1 instanceof StrikethroughSpan && s2 instanceof StrikethroughSpan)
            return true;
        if(s1 instanceof TypefaceSpan && s2 instanceof TypefaceSpan)
            return ((TypefaceSpan)s1).getFamily().equals(((TypefaceSpan)s2).getFamily());
        return false;
    }

    private void updateSpans() {
        Spannable text = getText();
        for (CharacterStyle style : text.getSpans(0, text.length(), CharacterStyle.class)) {
            if (text.getSpanFlags(style) == Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) {
                boolean found = false;
                for (SpanWrapper w : typingSpans) {
                    if (w.span == style) {
                        found = true;
                        break;
                    }
                }
                if (!found) {
                    SpanWrapper w = new SpanWrapper();
                    w.start = text.getSpanStart(style);
                    w.end = text.getSpanEnd(style);
                    w.span = style;
                    typingSpans.add(w);
                    //android.util.Log.d("IRCCloud", "Adding span at " + w.start + ", " + w.end);
                }
            }
        }

        ArrayList<SpanWrapper> spansToRemove = new ArrayList<>();
        for (SpanWrapper span : typingSpans) {
            for (SpanWrapper span2 : typingSpans) {
                if(!spansToRemove.contains(span) && span2 != span && span2.start == span.start && span2.end == span.end && compare((CharacterStyle)span2.span, (CharacterStyle)span.span))
                    spansToRemove.add(span2);
            }
        }

        if(spansToRemove.size() > 0) {
            //android.util.Log.d("IRCCloud", "Removing duplicates: " + spansToRemove);
            typingSpans.removeAll(spansToRemove);
        }

        //android.util.Log.d("IRCCloud", "Spans: " + typingSpans);
    }

    @Override
    public void onSelectionChanged(int start, int end) {
        if(selectionChangedByUser) {
            Spannable text = getText();
            if (text != null && text.length() > 0) {
                if (start != lastSelectionStart && end != lastSelectionEnd && (lastSelectionStart != start - 1 || lastSelectionStart != lastSelectionEnd || start != end)) {
                    if (typingEffects != null)
                        typingEffects.clear();
                    for (CharacterStyle style : text.getSpans(start, end, CharacterStyle.class)) {
                        if (text.getSpanFlags(style) == Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) {
                            text.removeSpan(style);
                        }
                    }
                    applyTypingSpans();
                    updateSpans();
                    for (CharacterStyle style : text.getSpans(start, end, CharacterStyle.class)) {
                        if (text.getSpanFlags(style) == Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) {
                            if (style instanceof StyleSpan && ((StyleSpan) style).getStyle() == Typeface.BOLD)
                                typingEffects.add(RichEditText.BOLD);
                            if (style instanceof StyleSpan && ((StyleSpan) style).getStyle() == Typeface.ITALIC)
                                typingEffects.add(RichEditText.ITALIC);
                            if (style instanceof UnderlineSpan)
                                typingEffects.add(RichEditText.UNDERLINE);
                            if (style instanceof ForegroundColorSpan) {
                                typingEffects.add(RichEditText.FOREGROUND);
                                typing_fg = ((ForegroundColorSpan) style).getForegroundColor();
                            }
                            if (style instanceof BackgroundColorSpan) {
                                typingEffects.add(RichEditText.BACKGROUND);
                                typing_bg = ((BackgroundColorSpan) style).getBackgroundColor();
                            }
                        }
                    }
                }
            }
        }
        lastSelectionStart = start;
        lastSelectionEnd = end;
        super.onSelectionChanged(start, end);
    }

    private void splitSpan(Spannable text, CharacterStyle style) {
        int start = text.getSpanStart(style);
        int end = text.getSpanEnd(style);
        text.removeSpan(style);

        if(start >= getSelectionStart() && end <= getSelectionEnd()) {
            for(SpanWrapper w : typingSpans) {
                if(w.span == style) {
                    typingSpans.remove(w);
                    break;
                }
            }
            return;
        }

        for(SpanWrapper w : typingSpans) {
            if(w.span == style) {
                w.start = start;
                w.end = end;
            }
        }
        if (start < getSelectionEnd() && end > getSelectionEnd()) {
            CharacterStyle newStyle;
            if(style instanceof ForegroundColorSpan)
                newStyle = new ForegroundColorSpan(((ForegroundColorSpan)style).getForegroundColor());
            else if(style instanceof BackgroundColorSpan)
                newStyle = new BackgroundColorSpan(((BackgroundColorSpan)style).getBackgroundColor());
            else if(style instanceof StyleSpan)
                newStyle = new StyleSpan(((StyleSpan)style).getStyle());
            else if(style instanceof UnderlineSpan)
                newStyle = new UnderlineSpan();
            else if(style instanceof StrikethroughSpan)
                newStyle = new StrikethroughSpan();
            else
                newStyle = CharacterStyle.wrap(style);
            text.setSpan(newStyle, getSelectionEnd(), end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            for(SpanWrapper w : typingSpans) {
                if(w.span == style) {
                    w = new SpanWrapper();
                    w.start = getSelectionEnd();
                    w.end = end;
                    w.span = newStyle;
                    typingSpans.add(w);
                    break;
                }
            }
        }
        if (start < getSelectionStart() && end > getSelectionStart()) {
            text.setSpan(style, start, getSelectionStart(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            for(SpanWrapper w : typingSpans) {
                if(w.span == style) {
                    w.start = start;
                    w.end = getSelectionStart();
                    break;
                }
            }
        }
    }

    private void splitInactiveSpans(Spannable text, int start, int end) {
        if(typingSpans != null) {
            updateSpans();
            applyTypingSpans();

            for (CharacterStyle style : text.getSpans(start, end, CharacterStyle.class)) {
                if (text.getSpanFlags(style) == Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) {
                    if (shouldRemoveSpan(style)) {
                        splitSpan(text, style);
                    }
                }
            }
        }
    }

    public void clearTypingEffects() {
        if(typingEffects != null)
            typingEffects.clear();
        if(typingSpans != null)
            typingSpans.clear();
        typing_fg = typing_bg = -1;

        splitInactiveSpans(getText(), getSelectionStart(), getSelectionEnd());
        super.onSelectionChanged(getSelectionStart(), getSelectionEnd());
        applyTypingSpans();
    }

    public void toggleTypingEffect(Effect effect) {
        if(typingEffects.contains(effect)) {
            typingEffects.remove(effect);
            if(typingSpans != null)
                typingSpans.clear();
            applyEffect(effect, false);
        } else {
            typingEffects.add(effect);
            applyEffect(effect, true);
        }
        splitInactiveSpans(getText(), getSelectionStart(), getSelectionEnd());
        super.onSelectionChanged(getSelectionStart(), getSelectionEnd());
    }

    public void applyForegroundColor(int color) {
        typing_fg = color;
        typingEffects.add(RichEditText.FOREGROUND);
        if(getSelectionEnd() <= getText().length())
            applyEffect(RichEditText.FOREGROUND, color);
        splitInactiveSpans(getText(), getSelectionStart(), getSelectionEnd());
        super.onSelectionChanged(getSelectionStart(), getSelectionEnd());
    }

    public void applyBackgroundColor(int color) {
        typing_bg = color;
        typingEffects.add(RichEditText.BACKGROUND);
        if(getSelectionEnd() <= getText().length())
            applyEffect(RichEditText.BACKGROUND, color);
        splitInactiveSpans(getText(), getSelectionStart(), getSelectionEnd());
        super.onSelectionChanged(getSelectionStart(), getSelectionEnd());
    }
} public class NetworkConnection {
    private static final String TAG = "IRCCloud";
    private static NetworkConnection instance = null;

    private static final ServersList mServers = ServersList.getInstance();
    private static final BuffersList mBuffers = BuffersList.getInstance();
    private static final ChannelsList mChannels = ChannelsList.getInstance();
    private static final UsersList mUsers = UsersList.getInstance();
    private static final EventsList mEvents = EventsList.getInstance();
    private static final RecentConversationsList mRecentConversations = RecentConversationsList.getInstance();

    public static final int STATE_DISCONNECTED = 0;
    public static final int STATE_CONNECTING = 1;
    public static final int STATE_CONNECTED = 2;
    public static final int STATE_DISCONNECTING = 3;
    private int state = STATE_DISCONNECTED;
    private long highest_eid = -1;

    public interface IRCEventHandler {
        void onIRCEvent(int message, Object object);
    }

    public interface IRCResultCallback {
        void onIRCResult(IRCCloudJSONObject result);
    }

    private WebSocketClient client = null;
    private UserInfo userInfo = null;
    private final ArrayList<IRCEventHandler> handlers = new ArrayList<IRCEventHandler>();
    private final HashMap<Integer, IRCResultCallback> resultCallbacks = new HashMap<>();
    public String session = null;
    private volatile int last_reqid = 0;
    private static final Timer idleTimer = new Timer("websocket-idle-timer");
    //private static final Timer saveTimer = new Timer("backlog-save-timer");
    private TimerTask idleTimerTask = null;
    private TimerTask saveTimerTask = null;
    private TimerTask disconnectSockerTimerTask = null;
    public long idle_interval = 1000;
    private volatile int failCount = 0;
    private long reconnect_timestamp = 0;
    public String useragent = null;
    private String streamId = null;
    private int accrued = 0;
    int currentBid = -1;
    long firstEid = -1;
    public JSONObject config = null;
    public boolean notifier;
    public static String file_uri_template;
    public static String pastebin_uri_template;
    public static String avatar_uri_template;
    public static String avatar_redirect_uri_template;

    private ObjectMapper mapper = new ObjectMapper();

    public static final int EVENT_CONNECTIVITY = 0;
    public static final int EVENT_USERINFO = 1;
    public static final int EVENT_MAKESERVER = 2;
    public static final int EVENT_MAKEBUFFER = 3;
    public static final int EVENT_DELETEBUFFER = 4;
    public static final int EVENT_BUFFERMSG = 5;
    public static final int EVENT_HEARTBEATECHO = 6;
    public static final int EVENT_CHANNELINIT = 7;
    public static final int EVENT_CHANNELTOPIC = 8;
    public static final int EVENT_JOIN = 9;
    public static final int EVENT_PART = 10;
    public static final int EVENT_NICKCHANGE = 11;
    public static final int EVENT_QUIT = 12;
    public static final int EVENT_MEMBERUPDATES = 13;
    public static final int EVENT_USERCHANNELMODE = 14;
    public static final int EVENT_BUFFERARCHIVED = 15;
    public static final int EVENT_BUFFERUNARCHIVED = 16;
    public static final int EVENT_RENAMECONVERSATION = 17;
    public static final int EVENT_STATUSCHANGED = 18;
    public static final int EVENT_CONNECTIONDELETED = 19;
    public static final int EVENT_AWAY = 20;
    public static final int EVENT_SELFBACK = 21;
    public static final int EVENT_KICK = 22;
    public static final int EVENT_CHANNELMODE = 23;
    public static final int EVENT_CHANNELTIMESTAMP = 24;
    public static final int EVENT_SELFDETAILS = 25;
    public static final int EVENT_USERMODE = 26;
    public static final int EVENT_SETIGNORES = 27;
    public static final int EVENT_BADCHANNELKEY = 28;
    public static final int EVENT_OPENBUFFER = 29;
    public static final int EVENT_BANLIST = 31;
    public static final int EVENT_WHOLIST = 32;
    public static final int EVENT_WHOIS = 33;
    public static final int EVENT_LINKCHANNEL = 34;
    public static final int EVENT_LISTRESPONSEFETCHING = 35;
    public static final int EVENT_LISTRESPONSE = 36;
    public static final int EVENT_LISTRESPONSETOOMANY = 37;
    public static final int EVENT_CONNECTIONLAG = 38;
    public static final int EVENT_GLOBALMSG = 39;
    public static final int EVENT_ACCEPTLIST = 40;
    public static final int EVENT_NAMESLIST = 41;
    public static final int EVENT_REORDERCONNECTIONS = 42;
    public static final int EVENT_CHANNELTOPICIS = 43;
    public static final int EVENT_SERVERMAPLIST = 44;
    public static final int EVENT_QUIETLIST = 45;
    public static final int EVENT_BANEXCEPTIONLIST = 46;
    public static final int EVENT_INVITELIST = 47;
    public static final int EVENT_CHANNELQUERY = 48;
    public static final int EVENT_WHOSPECIALRESPONSE = 49;
    public static final int EVENT_MODULESLIST = 50;
    public static final int EVENT_LINKSRESPONSE = 51;
    public static final int EVENT_WHOWAS = 52;
    public static final int EVENT_TRACERESPONSE = 53;
    public static final int EVENT_LOGEXPORTFINISHED = 54;
    public static final int EVENT_DISPLAYNAMECHANGE = 55;
    public static final int EVENT_AVATARCHANGE = 56;
    public static final int EVENT_MESSAGECHANGE = 57;
    public static final int EVENT_WATCHSTATUS = 58;
    public static final int EVENT_TEXTLIST = 59;
    public static final int EVENT_CHANFILTERLIST = 60;

    public static final int EVENT_BACKLOG_START = 100;
    public static final int EVENT_BACKLOG_END = 101;
    public static final int EVENT_BACKLOG_FAILED = 102;
    public static final int EVENT_AUTH_FAILED = 103;
    public static final int EVENT_TEMP_UNAVAILABLE = 104;
    public static final int EVENT_PROGRESS = 105;
    public static final int EVENT_ALERT = 106;
    public static final int EVENT_CACHE_START = 107;
    public static final int EVENT_CACHE_END = 108;
    public static final int EVENT_OOB_START = 109;
    public static final int EVENT_OOB_END = 110;
    public static final int EVENT_OOB_FAILED = 111;
    public static final int EVENT_FONT_DOWNLOADED = 112;

    public static final int EVENT_DEBUG = 999;

    public static String IRCCLOUD_HOST = BuildConfig.HOST;
    public static String IRCCLOUD_PATH = "/";

    public final Object parserLock = new Object();
    private WifiManager.WifiLock wifiLock = null;

    public long clockOffset = 0;

    private float numbuffers = 0;
    private float totalbuffers = 0;
    private int currentcount = 0;

    public boolean ready = false;
    public String globalMsg = null;

    private HashMap<Integer, OOBFetcher> oobTasks = new HashMap<Integer, OOBFetcher>();
    private ArrayList<IRCCloudJSONObject> pendingEdits = new ArrayList<>();

    public synchronized static NetworkConnection getInstance() {
        if (instance == null) {
            instance = new NetworkConnection();
        }
        return instance;
    }

    BroadcastReceiver connectivityListener = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            try {
                ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
                NetworkInfo ni = cm.getActiveNetworkInfo();

                if (intent.hasExtra("networkInfo") && ((NetworkInfo) intent.getParcelableExtra("networkInfo")).isConnected()) {
                    if (intent.getIntExtra("networkType", 0) == ConnectivityManager.TYPE_VPN) {
                        if (state == STATE_CONNECTED || state == STATE_CONNECTING) {
                            IRCCloudLog.Log(Log.INFO, TAG, "A VPN has connected, reconnecting websocket");
                            cancel_idle_timer();
                            reconnect_timestamp = 0;
                            try {
                                state = STATE_DISCONNECTING;
                                client.disconnect();
                                state = STATE_DISCONNECTED;
                                notifyHandlers(EVENT_CONNECTIVITY, null);
                            } catch (Exception e) {
                            }
                        }
                    }
                }

                IRCCloudLog.Log(Log.INFO, TAG, "Connectivity changed, connected: " + ((ni != null) ? ni.isConnected() : "Unknown") + ", connected or connecting: " + ((ni != null) ? ni.isConnectedOrConnecting() : "Unknown"));

                if (ni != null && ni.isConnected() && (state == STATE_DISCONNECTED || state == STATE_DISCONNECTING) && session != null && handlers.size() > 0 && !notifier) {
                    IRCCloudLog.Log(Log.INFO, TAG, "Network became available, reconnecting");
                    if (idleTimerTask != null)
                        idleTimerTask.cancel();
                    connect();
                } else if (ni == null || !ni.isConnected()) {
                    IRCCloudLog.Log(Log.INFO, TAG, "Network lost, disconnecting");
                    cancel_idle_timer();
                    reconnect_timestamp = 0;
                    try {
                        state = STATE_DISCONNECTING;
                        client.disconnect();
                        state = STATE_DISCONNECTED;
                        notifyHandlers(EVENT_CONNECTIVITY, null);
                    } catch (Exception e) {
                    }
                }
            } catch (Exception e) {
                printStackTraceToCrashlytics(e);
                IRCCloudLog.LogException(e);
            }
        }
    };

    private ConnectivityManager.NetworkCallback connectivityCallback;

    private BroadcastReceiver dataSaverListener = new BroadcastReceiver() {
        @TargetApi(24)
        @Override
        public void onReceive(Context context, Intent intent) {
            ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && cm.isActiveNetworkMetered() && cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED) {
                IRCCloudLog.Log(Log.INFO, TAG, "Data Saver was enabled");
                if(!isVisible() && state == STATE_CONNECTED) {
                    notifier = false;
                    disconnect();
                }
            } else {
                IRCCloudLog.Log(Log.INFO, TAG, "Data Saver was disabled");
                if(isVisible() && state != STATE_CONNECTED)
                    connect();
            }
        }
    };

    @SuppressWarnings("deprecation")
    public NetworkConnection() {
        session = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getString("session_key", "");
        String version;
        String network_type = null;
        try {
            version = "/" + IRCCloudApplication.getInstance().getPackageManager().getPackageInfo(IRCCloudApplication.getInstance().getApplicationContext().getPackageName(), 0).versionName;
        } catch (Exception e) {
            version = "";
        }

        try {
            ConnectivityManager cm = (ConnectivityManager) IRCCloudApplication.getInstance().getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo ni = cm.getActiveNetworkInfo();
            if (ni != null)
                network_type = ni.getTypeName();
        } catch (Exception e) {
        }

         connectivityCallback = new ConnectivityManager.NetworkCallback() {
            @Override
            public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) {
                super.onCapabilitiesChanged(network, networkCapabilities);

                /*if(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
                    if (state == STATE_CONNECTED || state == STATE_CONNECTING) {
                        IRCCloudLog.Log(Log.INFO, TAG, "A VPN has connected, reconnecting websocket");
                        cancel_idle_timer();
                        reconnect_timestamp = 0;
                        try {
                            state = STATE_DISCONNECTING;
                            client.disconnect();
                            state = STATE_DISCONNECTED;
                            notifyHandlers(EVENT_CONNECTIVITY, null);
                        } catch (Exception e) {
                        }
                    }
                }*/

                IRCCloudLog.Log(Log.INFO, TAG, "Connectivity changed, connected: " + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET));

                if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && (state == STATE_DISCONNECTED || state == STATE_DISCONNECTING) && session != null && handlers.size() > 0 && !notifier) {
                    IRCCloudLog.Log(Log.INFO, TAG, "Network became available, reconnecting");
                    if (idleTimerTask != null)
                        idleTimerTask.cancel();
                    connect();
                }
            }

            @Override
            public void onLost(Network network) {
                super.onLost(network);
                IRCCloudLog.Log(Log.INFO, TAG, "Network lost, disconnecting");
                cancel_idle_timer();
                reconnect_timestamp = 0;
                try {
                    state = STATE_DISCONNECTING;
                    client.disconnect();
                    state = STATE_DISCONNECTED;
                    notifyHandlers(EVENT_CONNECTIVITY, null);
                } catch (Exception e) {
                }
            }
        };

        try {
            config = new JSONObject(PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getString("config", "{}"));
            if(config.has("file_uri_template"))
                file_uri_template = config.getString("file_uri_template");
            else
                file_uri_template = null;

            if(config.has("pastebin_uri_template"))
                pastebin_uri_template = config.getString("pastebin_uri_template");
            else
                pastebin_uri_template = null;

            if(config.has("avatar_uri_template"))
                avatar_uri_template = config.getString("avatar_uri_template");
            else
                avatar_uri_template = null;

            if(config.has("avatar_redirect_uri_template"))
                avatar_redirect_uri_template = config.getString("avatar_redirect_uri_template");
            else
                avatar_redirect_uri_template = null;
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            config = new JSONObject();
        }

        String userinfojson = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getString("userinfo", null);
        if(userinfojson != null)
            userInfo = new UserInfo(new IRCCloudJSONObject(userinfojson));
        else if(BuildConfig.MOCK_DATA)
            userInfo = new UserInfo();

        useragent = "IRCCloud" + version + " (" + android.os.Build.MODEL + "; " + Locale.getDefault().getCountry().toLowerCase() + "; "
                + "Android " + android.os.Build.VERSION.RELEASE;

        WindowManager wm = (WindowManager) IRCCloudApplication.getInstance().getSystemService(Context.WINDOW_SERVICE);
        useragent += "; " + wm.getDefaultDisplay().getWidth() + "x" + wm.getDefaultDisplay().getHeight();

        if (network_type != null)
            useragent += "; " + network_type;

        useragent += ")";

        IRCCloudLog.Log(Log.INFO, "IRCCloud", useragent);

        WifiManager wfm = (WifiManager) IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.WIFI_SERVICE);
        wifiLock = wfm.createWifiLock(TAG);
    }

    public int getState() {
        return state;
    }

    public void disconnect() {
        if (client != null) {
            state = STATE_DISCONNECTING;
            client.disconnect();
        } else {
            state = STATE_DISCONNECTED;
        }
        if (idleTimerTask != null)
            idleTimerTask.cancel();
        IRCCloudApplication.getInstance().cancelNotifierTimer();
        try {
            if (wifiLock.isHeld())
                wifiLock.release();
        } catch (RuntimeException e) {

        }
        reconnect_timestamp = 0;
        synchronized (oobTasks) {
            for (Integer bid : oobTasks.keySet()) {
                try {
                    oobTasks.get(bid).cancel();
                } catch (Exception e) {
                }
            }
            oobTasks.clear();
        }
        for (Buffer b : mBuffers.getBuffers()) {
            if (!b.getScrolledUp())
                mEvents.pruneEvents(b.getBid());
        }
    }

    public JSONObject login(String email, String password) {
        try {
            String tokenResponse = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/auth-formtoken"), "", null, null, null);
            JSONObject token = new JSONObject(tokenResponse);
            if (token.has("token")) {
                String postdata = "email=" + URLEncoder.encode(email, "UTF-8") + "&password=" + URLEncoder.encode(password, "UTF-8") + "&token=" + token.getString("token");
                String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/login"), postdata, null, token.getString("token"), null);
                if (response.length() < 1) {
                    JSONObject o = new JSONObject();
                    o.put("message", "empty_response");
                    return o;
                } else if (response.charAt(0) != '{') {
                    JSONObject o = new JSONObject();
                    o.put("message", "invalid_response");
                    return o;
                }
                return new JSONObject(response);
            } else {
                return null;
            }
        } catch (UnknownHostException e) {
            printStackTraceToCrashlytics(e);
            return null;
        } catch (IOException e) {
            printStackTraceToCrashlytics(e);
            return null;
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            JSONObject o = new JSONObject();
            try {
                o.put("message", "json_error");
            } catch (JSONException e1) {
            }
            return o;
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject signup(String realname, String email, String password) {
        try {
            String tokenResponse = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/auth-formtoken"), "", null, null, null);
            JSONObject token = new JSONObject(tokenResponse);
            if (token.has("token")) {
                String postdata = "realname=" + URLEncoder.encode(realname, "UTF-8") + "&email=" + URLEncoder.encode(email, "UTF-8") + "&password=" + URLEncoder.encode(password, "UTF-8") + "&token=" + token.getString("token");
                String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/signup"), postdata, null, token.getString("token"), null);
                if (response.length() < 1) {
                    JSONObject o = new JSONObject();
                    o.put("message", "empty_response");
                    return o;
                } else if (response.charAt(0) != '{') {
                    JSONObject o = new JSONObject();
                    o.put("message", "invalid_response");
                    return o;
                }
                return new JSONObject(response);
            } else {
                return null;
            }
        } catch (UnknownHostException e) {
            printStackTraceToCrashlytics(e);
            return null;
        } catch (IOException e) {
            printStackTraceToCrashlytics(e);
            return null;
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            JSONObject o = new JSONObject();
            try {
                o.put("message", "json_error");
            } catch (JSONException e1) {
            }
            return o;
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject request_password(String email) {
        try {
            String tokenResponse = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/auth-formtoken"), "", null, null, null);
            JSONObject token = new JSONObject(tokenResponse);
            if (token.has("token")) {
                String postdata = "email=" + URLEncoder.encode(email, "UTF-8") + "&token=" + token.getString("token") + "&mobile=1";
                String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/request-access-link"), postdata, null, token.getString("token"), null);
                if (response.length() < 1) {
                    JSONObject o = new JSONObject();
                    o.put("message", "empty_response");
                    return o;
                } else if (response.charAt(0) != '{') {
                    JSONObject o = new JSONObject();
                    o.put("message", "invalid_response");
                    return o;
                }
                return new JSONObject(response);
            } else {
                return null;
            }
        } catch (UnknownHostException e) {
            printStackTraceToCrashlytics(e);
            return null;
        } catch (IOException e) {
            printStackTraceToCrashlytics(e);
            return null;
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            JSONObject o = new JSONObject();
            try {
                o.put("message", "json_error");
            } catch (JSONException e1) {
            }
            return o;
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject fetchJSON(String url) throws IOException {
        String response = null;
        try {
            response = fetch(new URL(url), null, session, null, null);
            return new JSONObject(response);
        } catch (Exception e) {
            if(response != null)
                IRCCloudLog.Log("Unable to parse JSON: " + response);
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject fetchJSON(String url, String postdata) throws IOException {
        String response = null;
        try {
            response = fetch(new URL(url), postdata, null, null, null);
            return new JSONObject(response);
        } catch (Exception e) {
            if(response != null)
                IRCCloudLog.Log("Unable to parse JSON: " + response);
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject fetchJSON(String url, HashMap<String, String>headers) throws IOException {
        String response = null;
        try {
            response = fetch(new URL(url), null, null, null, headers);
            return new JSONObject(response);
        } catch (Exception e) {
            if(response != null)
                IRCCloudLog.Log("Unable to parse JSON: " + response);
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public void fetchConfig(ConfigCallback callback) {
        IRCCloudLog.Log(Log.INFO, TAG, "Requesting configuration");
        try {
            new ConfigFetcher(new ConfigCallback() {
                @Override
                public void onConfig(JSONObject o) {
                    try {
                        if (o != null) {
                            config = o;
                            SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).edit();
                            prefs.putString("config", config.toString());
                            prefs.apply();

                            if (config.has("file_uri_template"))
                                file_uri_template = config.getString("file_uri_template");
                            else
                                file_uri_template = null;

                            if (config.has("pastebin_uri_template"))
                                pastebin_uri_template = config.getString("pastebin_uri_template");
                            else
                                pastebin_uri_template = null;

                            if (config.has("avatar_uri_template"))
                                avatar_uri_template = config.getString("avatar_uri_template");
                            else
                                avatar_uri_template = null;

                            if (config.has("avatar_redirect_uri_template"))
                                avatar_redirect_uri_template = config.getString("avatar_redirect_uri_template");
                            else
                                avatar_redirect_uri_template = null;

                            if (BuildConfig.ENTERPRISE && !(config.get("enterprise") instanceof JSONObject)) {
                                globalMsg = "Some features, such as push notifications, may not work as expected.  Please download the standard IRCCloud app from the <a href=\"" + config.getString("android_app") + "\">Play Store</a>";
                                notifyHandlers(EVENT_GLOBALMSG, null);
                            }
                            set_pastebin_cookie();

                            if (config.has("api_host")) {
                                set_api_host(config.getString("api_host"));
                            }
                        }
                        if(callback != null)
                            callback.onConfig(o);
                    } catch (Exception e) {
                        printStackTraceToCrashlytics(e);
                    }
                }
            }).connect();
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
    }

    public static void set_api_host(String host) {
        if (host.startsWith("http://"))
            host = host.substring(7);
        if (host.startsWith("https://"))
            host = host.substring(8);
        if (host.endsWith("/"))
            host = host.substring(0, host.length() - 1);

        SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit();
        editor.putString("host", host);
        editor.apply();
        NetworkConnection.IRCCLOUD_HOST = host;
        IRCCloudLog.Log(Log.INFO, TAG, "API host: " + NetworkConnection.IRCCLOUD_HOST);
    }

    public void set_pastebin_cookie() {
        try {
            if (config != null) {
                CookieSyncManager sm = CookieSyncManager.createInstance(IRCCloudApplication.getInstance().getApplicationContext());
                CookieManager cm = CookieManager.getInstance();
                Uri u = Uri.parse(config.getString("pastebin_uri_template"));
                cm.setCookie(u.getScheme() + "://" + u.getHost() + "/", "session=" + session);
                cm.flush();
                sm.sync();
                cm.setAcceptCookie(true);
            }
        } catch (Exception e) {
        }
    }

    public JSONObject registerGCM(String regId, String sk) throws IOException {
        String postdata = "device_id=" + regId + "&session=" + sk;
        try {
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/gcm-register"), postdata, sk, null, null);
            if(response.length() > 0)
                return new JSONObject(response);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject unregisterGCM(String regId, String sk) throws IOException {
        String postdata = "device_id=" + regId + "&session=" + sk;
        try {
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/gcm-unregister"), postdata, sk, null, null);
            if(response.length() > 0)
                return new JSONObject(response);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject files(int page) throws IOException {
        try {
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/files?page=" + page), null, session, null, null);
            if(response.length() > 0)
                return new JSONObject(response);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JsonNode propertiesForFile(String fileID) throws IOException {
        try {
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/file/json/" + fileID), null, session, null, null);
            if(response.length() > 0)
                return new ObjectMapper().readValue(response, JsonNode.class);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject pastebins(int page) throws IOException {
        try {
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/pastebins?page=" + page), null, session, null, null);
            if(response.length() > 0)
                return new JSONObject(response);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject logExports() throws IOException {
        try {
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/log-exports"), null, session, null, null);
            if(response.length() > 0)
                return new JSONObject(response);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public void logout(final String sk) {
        idleTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                try {
                    Log.i("IRCCloud", "Invalidating session");
                    fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/logout"), "session=" + sk, sk, null, null);
                } catch (Exception e) {
                    printStackTraceToCrashlytics(e);
                }
            }
        }, 50);
    }

    public JSONArray networkPresets() throws IOException {
        try {
            String response = fetch(new URL("https://www.irccloud.com/static/networks.json"), null, null, null, null);
            if(response.length() > 0) {
                JSONObject o = new JSONObject(response);
                return o.getJSONArray("networks");
            }
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    @TargetApi(24)
    public void registerForConnectivity() {
        try {
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                IntentFilter intentFilter = new IntentFilter();
                intentFilter.addAction(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED);
                IRCCloudApplication.getInstance().getApplicationContext().registerReceiver(dataSaverListener, intentFilter);

                ConnectivityManager cm = (ConnectivityManager) IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
                cm.registerDefaultNetworkCallback(connectivityCallback);
            } else {
                IntentFilter intentFilter = new IntentFilter();
                intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
                IRCCloudApplication.getInstance().getApplicationContext().registerReceiver(connectivityListener, intentFilter);
            }
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
    }

    public void unregisterForConnectivity() {
        try {
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                ConnectivityManager cm = (ConnectivityManager) IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
                cm.unregisterNetworkCallback(connectivityCallback);
            } else {
                IRCCloudApplication.getInstance().getApplicationContext().unregisterReceiver(connectivityListener);
            }
        } catch (IllegalArgumentException e) {
            //The broadcast receiver hasn't been registered yet
        }
        try {
            IRCCloudApplication.getInstance().getApplicationContext().unregisterReceiver(dataSaverListener);
        } catch (IllegalArgumentException e) {
            //The broadcast receiver hasn't been registered yet
        }
    }

    public void load() {
        /*notifyHandlers(EVENT_CACHE_START, null);
        try {
            String versionName = IRCCloudApplication.getInstance().getPackageManager().getPackageInfo(IRCCloudApplication.getInstance().getApplicationContext().getPackageName(), 0).versionName;
            SharedPreferences prefs = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0);
            if(!versionName.equals(prefs.getString("cacheVersion", ""))) {
                Log.w("IRCCloud", "App version changed, clearing cache");
                clearOfflineCache();
            }
        } catch (PackageManager.NameNotFoundException e) {
        }

        if(PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("enable_cache", true)) {
            mServers.load();
            mBuffers.load();
            mChannels.load();
            if (mServers.count() > 0) {
                String u = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getString("userinfo", null);
                if (u != null && u.length() > 0)
                    userInfo = new UserInfo(new IRCCloudJSONObject(u));
                highest_eid = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getLong("highest_eid", -1);
                streamId = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getString("streamId", null);
                ready = true;
            }
        }
        notifyHandlers(EVENT_CACHE_END, null);*/
    }

    public void save(int delay) {
        /*if (saveTimerTask != null)
            saveTimerTask.cancel();

        saveTimerTask = new TimerTask() {
            @Override
            public void run() {
                synchronized (saveTimer) {
                    saveTimerTask = null;
                    if (PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("enable_cache", true)) {
                        final long start = System.currentTimeMillis();
                        Log.i("IRCCloud", "Saving backlog");
                        final SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit();
                        editor.remove("streamId");
                        editor.remove("highest_eid");
                        editor.commit();

                        mUsers.save();
                        mEvents.save();
                        mChannels.save();
                        mBuffers.save();
                        mServers.save();
                        TransactionManager.getInstance().getSaveQueue().setTransactionListener(new TransactionListener<List<Model>>() {
                            @Override
                            public void onResultReceived(List<Model> models) {
                                Log.i("IRCCloud", "Saved " + models.size() + " objects in " + (System.currentTimeMillis() - start) + "ms");
                                if (handlers.size() == 0) {
                                    editor.putString("streamId", streamId);
                                    editor.putLong("highest_eid", highest_eid);
                                    try {
                                        editor.putString("cacheVersion", IRCCloudApplication.getInstance().getPackageManager().getPackageInfo(IRCCloudApplication.getInstance().getApplicationContext().getPackageName(), 0).versionName);
                                    } catch (PackageManager.NameNotFoundException e) {
                                        editor.remove("cacheVersion");
                                    }
                                    editor.commit();
                                }
                                TransactionManager.getInstance().getSaveQueue().setTransactionListener(null);
                            }

                            @Override
                            public boolean onReady(BaseTransaction<List<Model>> baseTransaction) {
                                return true;
                            }

                            @Override
                            public boolean hasResult(BaseTransaction<List<Model>> baseTransaction, List<Model> models) {
                                return true;
                            }
                        });
                        TransactionManager.getInstance().getSaveQueue().purgeQueue();
                    }
                }
            }
        };
        saveTimer.schedule(saveTimerTask, delay);*/
    }

    public void connect() {
        connect(false);
    }

    @TargetApi(24)
    public synchronized void connect(boolean ignoreNetworkState) {
        IRCCloudLog.Log(Log.DEBUG, TAG, "connect()");
        Context ctx = IRCCloudApplication.getInstance().getApplicationContext();
        session = ctx.getSharedPreferences("prefs", 0).getString("session_key", "");
        int limit = 100;

        if (session.length() == 0) {
            IRCCloudLog.Log(Log.INFO, TAG, "Session key not set");
            state = BuildConfig.MOCK_DATA ? STATE_CONNECTED : STATE_DISCONNECTED;
            notifyHandlers(EVENT_CONNECTIVITY, null);
            return;
        }

        ConnectivityManager cm = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
        if(cm != null) {
            NetworkInfo ni = cm.getActiveNetworkInfo();

            if (!ignoreNetworkState && ni != null && !ni.isConnectedOrConnecting()) {
                IRCCloudLog.Log(Log.INFO, TAG, "No active network connection");
                cancel_idle_timer();
                state = STATE_DISCONNECTED;
                reconnect_timestamp = 0;
                notifyHandlers(EVENT_CONNECTIVITY, null);
                return;
            }

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && cm.isActiveNetworkMetered() && cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED) {
                limit = 50;
            }

            if (ni != null && ni.getType() == ConnectivityManager.TYPE_MOBILE && (ni.getSubtype() == TelephonyManager.NETWORK_TYPE_EDGE || ni.getSubtype() == TelephonyManager.NETWORK_TYPE_GPRS || ni.getSubtype() == TelephonyManager.NETWORK_TYPE_CDMA || ni.getSubtype() == TelephonyManager.NETWORK_TYPE_1xRTT)) {
                limit = 25;
            }
        }

        if (state == STATE_CONNECTING || state == STATE_CONNECTED) {
            IRCCloudLog.Log(Log.INFO, TAG, "Ignoring duplicate connect request");
            return;
        }

        state = STATE_CONNECTING;

        if (saveTimerTask != null)
            saveTimerTask.cancel();
        saveTimerTask = null;

        if (oobTasks.size() > 0) {
            IRCCloudLog.Log(Log.DEBUG, TAG, "Clearing OOB tasks before connecting");
        }
        synchronized (oobTasks) {

            for (Integer bid : oobTasks.keySet()) {
                try {
                    oobTasks.get(bid).cancel();
                } catch (Exception e) {
                    printStackTraceToCrashlytics(e);
                }
            }
            oobTasks.clear();
        }

        reconnect_timestamp = 0;
        idle_interval = 0;
        accrued = 0;
        resultCallbacks.clear();
        notifyHandlers(EVENT_CONNECTIVITY, null);

        fetchConfig(new ConnectCallback(limit));
    }

    public interface ConfigCallback {
        void onConfig(JSONObject config);
    }

    private class ConfigFetcher extends HTTPFetcher {
        ConfigCallback callback;
        JSONObject result = null;

        public ConfigFetcher(ConfigCallback callback) throws MalformedURLException {
            super(new URL("https://" + IRCCLOUD_HOST + "/config"));
            this.callback = callback;
        }

        protected void onFetchComplete() {
            if(!isCancelled && callback != null)
                callback.onConfig(result);
        }

        protected void onFetchFailed() {
            if(!isCancelled && callback != null)
                callback.onConfig(result);
        }

        protected void onStreamConnected(InputStream is) throws Exception {
            if (isCancelled)
                return;

            ByteArrayOutputStream os = new ByteArrayOutputStream();
            byte[] buffer = new byte[8192];
            int len;
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            String response = os.toString("UTF-8");
            is.close();

            result = new JSONObject(response);
        }
    }

    private class ConnectCallback implements ConfigCallback {
        int limit;

        public ConnectCallback(int limit) {
            this.limit = limit;
        }

        @Override
        public void onConfig(JSONObject config) {
            try {
                if (config != null) {
                    String host = null;
                    int port = -1;
                    host = System.getProperty("http.proxyHost", null);
                    try {
                        port = Integer.parseInt(System.getProperty("http.proxyPort", "8080"));
                    } catch (NumberFormatException e) {
                        port = -1;
                    }

                    if (!wifiLock.isHeld())
                        wifiLock.acquire();

                    Map<String, String> extraHeaders = new HashMap<>();
                    extraHeaders.put("User-Agent", useragent);

                    String url = "wss://" + config.getString("socket_host") + IRCCLOUD_PATH;
                    if (highest_eid > 0 && streamId != null && streamId.length() > 0) {
                        url += "?exclude_archives=1&since_id=" + highest_eid + "&stream_id=" + streamId;
                        if (notifier)
                            url += "&notifier=1";
                        url += "&limit=" + limit;
                    } else if (notifier) {
                        url += "?exclude_archives=1&notifier=1&limit=" + limit;
                    } else {
                        url += "?exclude_archives=1&limit=" + limit;
                    }

                    if (host != null && host.length() > 0 && !host.equalsIgnoreCase("localhost") && !host.equalsIgnoreCase("127.0.0.1") && port > 0) {
                        IRCCloudLog.Log(Log.DEBUG, TAG, "Connecting via proxy: " + host);
                    }

                    IRCCloudLog.Log(Log.DEBUG, TAG, "Attempt: " + failCount);

                    if (client != null) {
                        client.setListener(null);
                        client.disconnect();
                    }

                    TrustManager[] trustManagers = new TrustManager[1];
                    trustManagers[0] = TrustKit.getInstance().getTrustManager(config.getString("socket_host"));
                    WebSocketClient.setTrustManagers(trustManagers);
                    HttpMetric m = null;

                    try {
                        m = FirebasePerformance.getInstance().newHttpMetric(url.replace("wss://", "https://"), FirebasePerformance.HttpMethod.GET);
                        m.start();
                    } catch (Exception e) {

                    }

                    final HttpMetric metric = m;
                    client = new WebSocketClient(URI.create(url), new WebSocketClient.Listener() {
                        @Override
                        public void onConnect() {
                            if (client != null && client.getListener() == this) {
                                IRCCloudLog.Log(Log.DEBUG, TAG, "WebSocket connected");
                                if (metric != null) {
                                    metric.setHttpResponseCode(200);
                                    metric.stop();
                                }
                                try {
                                    JSONObject o = new JSONObject();
                                    o.put("cookie", session);
                                    send("auth", o, null);
                                } catch (JSONException e) {
                                    printStackTraceToCrashlytics(e);
                                }

                                IRCCloudLog.Log(Log.DEBUG, TAG, "Emptying cache");
                                if (saveTimerTask != null)
                                    saveTimerTask.cancel();
                                saveTimerTask = null;
                                final SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit();
                                editor.remove("streamId");
                                editor.remove("highest_eid");
                                editor.apply();
                                //Delete.tables(Server.class, Buffer.class, Channel.class);
                                notifyHandlers(EVENT_CONNECTIVITY, null);
                                if (disconnectSockerTimerTask != null)
                                    disconnectSockerTimerTask.cancel();
                                if (notifier) {
                                    disconnectSockerTimerTask = new TimerTask() {
                                        @Override
                                        public void run() {
                                            disconnectSockerTimerTask = null;
                                            if (notifier) {
                                                Log.d("IRCCloud", "Notifier socket expired");
                                                disconnect();
                                            }
                                        }
                                    };
                                    idleTimer.schedule(disconnectSockerTimerTask, 600000);
                                }
                            } else {
                                IRCCloudLog.Log(Log.WARN, "IRCCloud", "Got websocket onConnect for inactive websocket");
                            }
                        }

                        @Override
                        public void onMessage(String message) {
                            if (client != null && client.getListener() == this && message.length() > 0) {
                                try {
                                    synchronized (parserLock) {
                                        parse_object(new IRCCloudJSONObject(mapper.readValue(message, JsonNode.class)), false);
                                    }
                                } catch (Exception e) {
                                    Log.e(TAG, "Unable to parse: " + message);
                                    IRCCloudLog.LogException(e);
                                    printStackTraceToCrashlytics(e);
                                }
                            }
                        }

                        @Override
                        public void onMessage(byte[] data) {
                            //Log.d(TAG, String.format("Got binary message! %s", toHexString(data));
                        }

                        private void closed() {
                            try {
                                if (wifiLock.isHeld())
                                    wifiLock.release();
                            } catch (RuntimeException e) {

                            }

                            IRCCloudLog.Log(Log.DEBUG, TAG, "Clearing OOB tasks");
                            synchronized (oobTasks) {
                                for (Integer bid : oobTasks.keySet()) {
                                    try {
                                        oobTasks.get(bid).cancel();
                                    } catch (Exception e) {
                                        printStackTraceToCrashlytics(e);
                                    }
                                }
                                oobTasks.clear();
                            }
                            IRCCloudLog.Log(Log.DEBUG, TAG, "Clear");

                            if (highest_eid <= 0)
                                streamId = null;

                            ConnectivityManager cm = (ConnectivityManager) IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
                            NetworkInfo ni = cm.getActiveNetworkInfo();
                            if (state == STATE_DISCONNECTING || ni == null || !ni.isConnected()) {
                                cancel_idle_timer();
                                state = STATE_DISCONNECTED;
                            } else {
                                fail();
                            }
                        }

                        @Override
                        public void onDisconnect(int code, String reason) {
                            if (client != null && client.getListener() == this) {
                                IRCCloudLog.Log(Log.DEBUG, TAG, "WebSocket disconnected: " + code + " " + reason);
                                closed();
                            } else {
                                IRCCloudLog.Log(Log.WARN, "IRCCloud", "Got websocket onDisconnect for inactive websocket");
                            }
                        }

                        @Override
                        public void onError(Exception error) {
                            if (client != null && client.getListener() == this) {
                                IRCCloudLog.Log(Log.ERROR, TAG, "The WebSocket encountered an error: " + error.toString());
                                closed();
                            } else {
                                IRCCloudLog.Log(Log.WARN, "IRCCloud", "Got websocket onError for inactive websocket");
                            }
                        }
                    }, extraHeaders);

                    if (client != null) {
                        client.setDebugListener(new WebSocketClient.DebugListener() {
                            @Override
                            public void onDebugMsg(String msg) {
                                IRCCloudLog.Log(Log.DEBUG, "IRCCloud", msg);
                            }
                        });
                        if (host != null && host.length() > 0 && !host.equalsIgnoreCase("localhost") && !host.equalsIgnoreCase("127.0.0.1") && port > 0)
                            client.setProxy(host, port);
                        else
                            client.setProxy(null, -1);
                        client.connect();
                    }
                } else {
                    IRCCloudLog.Log(Log.ERROR, TAG, "Unable to fetch configuration");
                    fail();
                }
            } catch (Exception e) {
                printStackTraceToCrashlytics(e);
                fail();
            }
        }
    }

    private void fail() {
        failCount++;
        if (failCount < 4)
            idle_interval = failCount * 1000;
        else if (failCount < 10)
            idle_interval = 10000;
        else
            idle_interval = 30000;
        schedule_idle_timer();
        IRCCloudLog.Log(Log.DEBUG, TAG, "Reconnecting in " + idle_interval / 1000 + " seconds");
        state = STATE_DISCONNECTED;
        notifyHandlers(EVENT_CONNECTIVITY, null);
    }

    public void logout() {
        streamId = null;
        disconnect();
        try {
            if(IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getString("gcm_id", "").length() > 0) {
                BackgroundTaskWorker.unregisterGCM();
            }
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        ready = false;
        accrued = 0;
        highest_eid = -1;
        SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).edit();
        prefs.remove("uid");
        prefs.remove("name");
        prefs.remove("email");
        prefs.remove("highlights");
        prefs.remove("theme");
        prefs.remove("monospace");
        prefs.apply();
        SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit();
        editor.clear();
        editor.apply();
        mServers.clear();
        mBuffers.clear();
        mChannels.clear();
        mUsers.clear();
        mEvents.clear();
        pendingEdits.clear();
        NotificationsList.getInstance().clear();
        NotificationsList.getInstance().pruneNotificationChannels();
        userInfo = null;
        session = null;
        ImageList.getInstance().purge();
        AvatarsList.getInstance().clear();
        mRecentConversations.clear();
        LogExportsList.getInstance().clear();
        FirebaseAnalytics.getInstance(IRCCloudApplication.getInstance().getApplicationContext()).resetAnalyticsData();
        IRCCloudLog.clear();
        if(!BuildConfig.ENTERPRISE)
            IRCCLOUD_HOST = BuildConfig.HOST;
        save(100);
    }

    public void clearOfflineCache() {
        SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit();
        editor.remove("userinfo");
        editor.remove("highest_eid");
        editor.remove("streamId");
        editor.apply();
        highest_eid = -1;
        streamId = null;
        disconnect();
        mServers.clear();
        mBuffers.clear();
        mChannels.clear();
        mUsers.clear();
        mEvents.clear();
        pendingEdits.clear();
        connect();
    }

    private int send(String method, JSONObject params, IRCResultCallback callback) {
        if (client == null || (state != STATE_CONNECTED && !method.equals("auth")) || BuildConfig.MOCK_DATA)
            return -1;
        synchronized (resultCallbacks) {
            try {
                params.put("_reqid", ++last_reqid);
                if (callback != null)
                    resultCallbacks.put(last_reqid, callback);
                params.put("_method", method);
                //Log.d(TAG, "Reqid: " + last_reqid + " Method: " + method + " Params: " + params.toString());
                client.send(params.toString());
                return last_reqid;
            } catch (Exception e) {
                printStackTraceToCrashlytics(e);
                return -1;
            }
        }
    }

    public int heartbeat(int cid, int bid, long last_seen_eid, IRCResultCallback callback) {
        return heartbeat(bid, new Integer[]{cid}, new Integer[]{bid}, new Long[]{last_seen_eid}, callback);
    }

    public int heartbeat(int selectedBuffer, Integer cids[], Integer bids[], Long last_seen_eids[], IRCResultCallback callback) {
        try {
            JSONObject heartbeat = new JSONObject();
            for (int i = 0; i < cids.length; i++) {
                JSONObject o;
                if (heartbeat.has(String.valueOf(cids[i]))) {
                    o = heartbeat.getJSONObject(String.valueOf(cids[i]));
                } else {
                    o = new JSONObject();
                    heartbeat.put(String.valueOf(cids[i]), o);
                }
                o.put(String.valueOf(bids[i]), last_seen_eids[i]);
            }

            JSONObject o = new JSONObject();
            o.put("selectedBuffer", selectedBuffer);
            o.put("seenEids", heartbeat.toString());
            return send("heartbeat", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int disconnect(int cid, String message, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("msg", message);
            return send("disconnect", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int reconnect(int cid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            int reqid = send("reconnect", o, callback);
            if(reqid > 0) {
                Server s = mServers.getServer(cid);
                if(s != null) {
                    s.setStatus("queued");
                    notifyHandlers(EVENT_CONNECTIONLAG, new IRCCloudJSONObject(o));
                }
            }
            return reqid;
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int say(int cid, String to, String message, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            if (to != null)
                o.put("to", to);
            else
                o.put("to", "*");
            o.put("msg", message);
            return send("say", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int reply(int cid, String to, String message, String msgid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("to", to);
            o.put("reply", message);
            o.put("msgid", msgid);
            return send("reply", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int edit_message(int cid, String to, String message, String msgid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("to", to);
            o.put("edit", message);
            o.put("msgid", msgid);
            return send("edit-message", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int delete_message(int cid, String to, String msgid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("to", to);
            o.put("msgid", msgid);
            return send("delete-message", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public JSONObject postSay(int cid, String to, String message, String sk) throws IOException {
        if(to == null)
            to = "*";
        String postdata = "cid=" + cid + "&to=" + URLEncoder.encode(to, "UTF-8") + "&msg=" + URLEncoder.encode(message, "UTF-8") + "&session=" + sk;
        try {
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/say"), postdata, sk, null, null);
            return new JSONObject(response);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }

    public JSONObject postHeartbeat(int cid, int bid, long last_seen_eid, String sk) {
        return postHeartbeat(bid, new Integer[]{cid}, new Integer[]{bid}, new Long[]{last_seen_eid}, sk);
    }

    public JSONObject postHeartbeat(int selectedBuffer, Integer cids[], Integer bids[], Long last_seen_eids[], String sk) {
        try {
            JSONObject heartbeat = new JSONObject();
            for (int i = 0; i < cids.length; i++) {
                JSONObject o;
                if (heartbeat.has(String.valueOf(cids[i]))) {
                    o = heartbeat.getJSONObject(String.valueOf(cids[i]));
                } else {
                    o = new JSONObject();
                    heartbeat.put(String.valueOf(cids[i]), o);
                }
                o.put(String.valueOf(bids[i]), last_seen_eids[i]);
            }

            String postdata = "selectedBuffer=" + selectedBuffer + "&seenEids=" + URLEncoder.encode(heartbeat.toString(), "UTF-8") + "&session=" + sk;
            String response = fetch(new URL("https://" + IRCCLOUD_HOST + "/chat/heartbeat"), postdata, sk, null, null);
            return new JSONObject(response);
        } catch (Exception e) {
            printStackTraceToCrashlytics(e);
        }
        return null;
    }


    public int join(int cid, String channel, String key, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("channel", channel);
            o.put("key", key);
            return send("join", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int part(int cid, String channel, String message, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("channel", channel);
            o.put("msg", message);
            return send("part", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int kick(int cid, String channel, String nick, String message, IRCResultCallback callback) {
        return say(cid, channel, "/kick " + nick + " " + message, callback);
    }

    public int mode(int cid, String channel, String mode, IRCResultCallback callback) {
        return say(cid, channel, "/mode " + channel + " " + mode, callback);
    }

    public int invite(int cid, String channel, String nick, IRCResultCallback callback) {
        return say(cid, channel, "/invite " + nick + " " + channel, callback);
    }

    public int archiveBuffer(int cid, long bid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("id", bid);
            return send("archive-buffer", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int unarchiveBuffer(int cid, long bid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("id", bid);
            return send("unarchive-buffer", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int renameChannel(String name, int cid, long bid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("id", bid);
            o.put("name", name);
            return send("rename-channel", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int renameConversation(String name, int cid, long bid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("id", bid);
            o.put("name", name);
            return send("rename-conversation", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int deleteBuffer(int cid, long bid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("id", bid);
            return send("delete-buffer", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int deleteServer(int cid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            return send("delete-connection", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int deleteFile(String id, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("file", id);
            return send("delete-file", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int restoreFile(String id, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("file", id);
            return send("restore-file", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int addServer(String hostname, int port, int ssl, String netname, String nickname, String realname, String server_pass, String nickserv_pass, String joincommands, String channels, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("hostname", hostname);
            o.put("port", port);
            o.put("ssl", String.valueOf(ssl));
            if (netname != null)
                o.put("netname", netname);
            o.put("nickname", nickname);
            o.put("realname", realname);
            o.put("server_pass", server_pass);
            o.put("nspass", nickserv_pass);
            o.put("joincommands", joincommands);
            o.put("channels", channels);
            return send("add-server", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int editServer(int cid, String hostname, int port, int ssl, String netname, String nickname, String realname, String server_pass, String nickserv_pass, String joincommands, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("hostname", hostname);
            o.put("port", port);
            o.put("ssl", String.valueOf(ssl));
            if (netname != null)
                o.put("netname", netname);
            o.put("nickname", nickname);
            o.put("realname", realname);
            o.put("server_pass", server_pass);
            o.put("nspass", nickserv_pass);
            o.put("joincommands", joincommands);
            o.put("cid", cid);
            return send("edit-server", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int ignore(int cid, String mask, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("mask", mask);
            return send("ignore", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int unignore(int cid, String mask, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("mask", mask);
            return send("unignore", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int set_prefs(String prefs, IRCResultCallback callback) {
        try {
            Log.i("IRCCloud", "Setting prefs: " + prefs);
            JSONObject o = new JSONObject();
            o.put("prefs", prefs);
            if(BuildConfig.MOCK_DATA) {
                if(userInfo == null)
                    userInfo = new UserInfo();
                userInfo.prefs = new JSONObject(prefs);
                notifyHandlers(EVENT_USERINFO, userInfo);
            }
            return send("set-prefs", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int set_user_settings(String realname, String hwords, boolean autoaway, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("realname", realname);
            o.put("hwords", hwords);
            o.put("autoaway", autoaway ? "1" : "0");
            return send("user-settings", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int change_password(String email, String oldPassword, String newPassword, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            if(email != null)
                o.put("email", email);
            o.put("password", oldPassword);
            if(newPassword != null)
                o.put("newpassword", newPassword);
            return send("change-password", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int delete_account(String password, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("password", password);
            return send("delete-account", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int ns_help_register(int cid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            return send("ns-help-register", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int set_nspass(int cid, String nspass, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("nspass", nspass);
            return send("set-nspass", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int whois(int cid, String nick, String server, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("nick", nick);
            if (server != null)
                o.put("server", server);
            return send("whois", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int topic(int cid, String channel, String topic, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("channel", channel);
            o.put("topic", topic);
            return send("topic", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int back(int cid, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            return send("back", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int reorder_connections(String cids, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cids", cids);
            return send("reorder-connections", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int resend_verify_email(IRCResultCallback callback) {
        JSONObject o = new JSONObject();
        return send("resend-verify-email", o, callback);
    }

    public int finalize_upload(String id, String filename, String original_filename, boolean avatar, int cid, int orgId, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("id", id);
            o.put("filename", filename);
            o.put("original_filename", original_filename);
            if(avatar) {
                o.put("type", "avatar");
                if(orgId == -1) {
                    o.put("primary", "1");
                } else if(orgId == 0) {
                    o.put("cid", cid);
                } else {
                    o.put("org", orgId);
                }
            }
            return send("upload-finalise", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int paste(String name, String extension, String contents, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            if(name != null && name.length() > 0)
                o.put("name", name);
            o.put("contents", contents);
            o.put("extension", extension);
            return send("paste", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int edit_paste(String id, String name, String extension, String contents, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("id", id);
            if(name != null && name.length() > 0)
                o.put("name", name);
            o.put("body", contents);
            o.put("extension", extension);
            return send("edit-pastebin", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int delete_paste(String id, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("id", id);
            return send("delete-pastebin", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int export_log(int cid, int bid, String timezone, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            if(cid > 0)
                o.put("cid", cid);
            if(bid > 0)
                o.put("bid", bid);
            o.put("timezone", timezone);
            return send("export-log", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int set_avatar(int cid, int orgId, String avatar_id, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            if(avatar_id != null)
                o.put("id", avatar_id);
            else
                o.put("clear", "1");

            if(orgId == 0)
                o.put("cid", cid);
            else if(orgId != -1)
                o.put("org", orgId);
            else
                o.put("primary", "1");
            return send("set-avatar", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public int set_netname(int cid, String name, IRCResultCallback callback) {
        try {
            JSONObject o = new JSONObject();
            o.put("cid", cid);
            o.put("netname", name);
            return send("set-netname", o, callback);
        } catch (JSONException e) {
            printStackTraceToCrashlytics(e);
            return -1;
        }
    }

    public void request_backlog(int cid, int bid, long beforeId) {
        try {
            synchronized (oobTasks) {
                if (oobTasks.containsKey(bid)) {
                    IRCCloudLog.Log(Log.WARN, TAG, "Ignoring duplicate backlog request for BID: " + bid);
                    return;
                }
            }
            if (session == null || session.length() == 0) {
                IRCCloudLog.Log(Log.WARN, TAG, "Not fetching backlog before session is set");
                return;
            }
            if (Looper.myLooper() == null)
                Looper.prepare();

            OOBFetcher task;
            if (beforeId > 0)
                task = new OOBFetcher(new URL("https://" + IRCCLOUD_HOST + "/chat/backlog?cid=" + cid + "&bid=" + bid + "&beforeid=" + beforeId), bid);
            else
                task = new OOBFetcher(new URL("https://" + IRCCLOUD_HOST + "/chat/backlog?cid=" + cid + "&bid=" + bid), bid);
            synchronized (oobTasks) {
                oobTasks.put(bid, task);
                if(oobTasks.size() == 1)
                    task.connect();
            }
        } catch (MalformedURLException e) {
            printStackTraceToCrashlytics(e);
        }
    }

    public void request_archives(int cid) {
        try {
            synchronized (oobTasks) {
                if (oobTasks.containsKey(cid)) {
                    IRCCloudLog.Log(Log.WARN, TAG, "Ignoring duplicate archives request for CID: " + cid);
                    return;
                }
            }
            if (session == null || session.length() == 0) {
                IRCCloudLog.Log(Log.WARN, TAG, "Not fetching archives before session is set");
                return;
            }

            OOBFetcher task = new OOBFetcher(new URL("https://" + IRCCLOUD_HOST + "/chat/archives?cid=" + cid), cid);
            synchronized (oobTasks) {
                oobTasks.put(cid, task);
                if(oobTasks.size() == 1)
                    task.connect();
            }
        } catch (MalformedURLException e) {
            printStackTraceToCrashlytics(e);
        }
    }

    public void request_mock_data() {
        try {
            OOBFetcher task = new OOBFetcher(new URL("https://www.irccloud.com/test/bufferview.json"), -1);
            synchronized (oobTasks) {
                oobTasks.put(-1, task);
                if(oobTasks.size() == 1)
                    task.connect();
            }
        } catch (MalformedURLException e) {
            printStackTraceToCrashlytics(e);
        }
    }

    public void upgrade() {
        if (disconnectSockerTimerTask != null)
            disconnectSockerTimerTask.cancel();
        notifier = false;
        send("upgrade_notifier", new JSONObject(), null);
    }

    public void cancel_idle_timer() {
        if (idleTimerTask != null)
            idleTimerTask.cancel();
        reconnect_timestamp = 0;
    }

    public void schedule_idle_timer() {
        if (idleTimerTask != null)
            idleTimerTask.cancel();
        if (idle_interval <= 0 || BuildConfig.MOCK_DATA)
            return;

        try {
            idleTimerTask = new TimerTask() {
                public void run() {
                    if (handlers.size() > 0) {
                        IRCCloudLog.Log(Log.INFO, TAG, "Websocket idle time exceeded, reconnecting...");
                        state = STATE_DISCONNECTING;
                        notifyHandlers(EVENT_CONNECTIVITY, null);
                        if (client != null)
                            client.disconnect();
                        connect();
                    }
                    reconnect_timestamp = 0;
                }
            };
            idleTimer.schedule(idleTimerTask, idle_interval);
            reconnect_timestamp = System.currentTimeMillis() + idle_interval;
        } catch (IllegalStateException e) {
            //It's possible for timer to get canceled by another thread before before it gets scheduled
            //so catch the exception
        }
    }

    public long getReconnectTimestamp() {
        return reconnect_timestamp;
    }

    public interface Parser {
        void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException;
    }

    private class BroadcastParser implements Parser {
        int type;

        BroadcastParser(int t) {
            type = t;
        }

        public void parse(IRCCloudJSONObject object, boolean backlog) {
            if (!backlog)
                notifyHandlers(type, object);
        }
    }

    public HashMap<String, Parser> parserMap = new HashMap<String, Parser>() {{
        //Ignored events
        put("idle", null);
        put("end_of_backlog", null);
        put("user_account", null);
        put("twitch_hosttarget_start", null);
        put("twitch_hosttarget_stop", null);
        put("twitch_usernotice", null);

        put("header", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                if (!(object.has("resumed") && object.getBoolean("resumed"))) {
                    Log.d("IRCCloud", "Socket was not resumed");
                    NotificationsList.getInstance().clearLastSeenEIDs();
                    mEvents.clear();
                    pendingEdits.clear();
                    if(streamId != null) {
                        IRCCloudLog.Log(Log.WARN, "IRCCloud", "Unable to resume socket, requesting full OOB load");
                        highest_eid = 0;
                        streamId = null;
                        failCount = 0;
                        ready = false;
                        if (client != null)
                            client.disconnect();
                        return;
                    }
                }
                idle_interval = object.getLong("idle_interval") + 15000;
                clockOffset = object.getLong("time") - (System.currentTimeMillis() / 1000);
                currentcount = 0;
                currentBid = -1;
                firstEid = -1;
                streamId = object.getString("streamid");
                if (object.has("accrued"))
                    accrued = object.getInt("accrued");
                state = STATE_CONNECTED;
                ImageList.getInstance().clear();
                ImageList.getInstance().clearFailures();
                notifyHandlers(EVENT_CONNECTIVITY, null);
            }
        });

        put("global_system_message", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                String msgType = object.getString("system_message_type");
                if (msgType == null || (!msgType.equalsIgnoreCase("eval") && !msgType.equalsIgnoreCase("refresh"))) {
                    globalMsg = object.getString("msg");
                    notifyHandlers(EVENT_GLOBALMSG, object);
                }
            }
        });

        put("num_invites", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                if (userInfo != null)
                    userInfo.num_invites = object.getInt("num_invites");
            }
        });

        put("stat_user", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                userInfo = new UserInfo(object);
                if(IRCCloudLog.CrashlyticsEnabled)
                    FirebaseCrashlytics.getInstance().setUserId("uid" + userInfo.id);
                SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext());
                SharedPreferences.Editor editor = prefs.edit();
                editor.putString("uid", "uid" + userInfo.id);
                editor.putString("name", userInfo.name);
                editor.putString("email", userInfo.email);
                editor.putString("highlights", userInfo.highlights);
                editor.putBoolean("autoaway", userInfo.auto_away);
                if (userInfo.prefs != null && !BuildConfig.MOCK_DATA) {
                    editor.putBoolean("time-24hr", userInfo.prefs.has("time-24hr") && userInfo.prefs.get("time-24hr") instanceof Boolean && userInfo.prefs.getBoolean("time-24hr"));
                    editor.putBoolean("time-seconds", userInfo.prefs.has("time-seconds") && userInfo.prefs.get("time-seconds") instanceof Boolean && userInfo.prefs.getBoolean("time-seconds"));
                    editor.putBoolean("mode-showsymbol", userInfo.prefs.has("mode-showsymbol") && userInfo.prefs.get("mode-showsymbol") instanceof Boolean && userInfo.prefs.getBoolean("mode-showsymbol"));
                    editor.putBoolean("nick-colors", userInfo.prefs.has("nick-colors") && userInfo.prefs.get("nick-colors") instanceof Boolean && userInfo.prefs.getBoolean("nick-colors"));
                    editor.putBoolean("mention-colors", userInfo.prefs.has("mention-colors") && userInfo.prefs.get("mention-colors") instanceof Boolean && userInfo.prefs.getBoolean("mention-colors"));
                    editor.putBoolean("emoji-disableconvert", !(userInfo.prefs.has("emoji-disableconvert") && userInfo.prefs.get("emoji-disableconvert") instanceof Boolean && userInfo.prefs.getBoolean("emoji-disableconvert")));
                    editor.putBoolean("pastebin-disableprompt", !(userInfo.prefs.has("pastebin-disableprompt") && userInfo.prefs.get("pastebin-disableprompt") instanceof Boolean && userInfo.prefs.getBoolean("pastebin-disableprompt")));
                    editor.putBoolean("hideJoinPart", !(userInfo.prefs.has("hideJoinPart") && userInfo.prefs.get("hideJoinPart") instanceof Boolean && userInfo.prefs.getBoolean("hideJoinPart")));
                    editor.putBoolean("expandJoinPart", !(userInfo.prefs.has("expandJoinPart") && userInfo.prefs.get("expandJoinPart") instanceof Boolean && userInfo.prefs.getBoolean("expandJoinPart")));
                    editor.putBoolean("notifications_all", (userInfo.prefs.has("notifications-all") && userInfo.prefs.get("notifications-all") instanceof Boolean && userInfo.prefs.getBoolean("notifications-all")));
                    editor.putBoolean("notifications_mute", (userInfo.prefs.has("notifications-mute") && userInfo.prefs.get("notifications-mute") instanceof Boolean && userInfo.prefs.getBoolean("notifications-mute")));
                    editor.putBoolean("disableTrackUnread", !(userInfo.prefs.has("disableTrackUnread") && userInfo.prefs.get("disableTrackUnread") instanceof Boolean && userInfo.prefs.getBoolean("disableTrackUnread")));
                    editor.putBoolean("enableReadOnSelect", (userInfo.prefs.has("enableReadOnSelect") && userInfo.prefs.get("enableReadOnSelect") instanceof Boolean && userInfo.prefs.getBoolean("enableReadOnSelect")));
                    editor.putBoolean("ascii-compact", (userInfo.prefs.has("ascii-compact") && userInfo.prefs.get("ascii-compact") instanceof Boolean && userInfo.prefs.getBoolean("ascii-compact")));
                    editor.putBoolean("emoji-nobig", !(userInfo.prefs.has("emoji-nobig") && userInfo.prefs.get("emoji-nobig") instanceof Boolean && userInfo.prefs.getBoolean("emoji-nobig")));
                    editor.putBoolean("files-disableinline", !(userInfo.prefs.has("files-disableinline") && userInfo.prefs.get("files-disableinline") instanceof Boolean && userInfo.prefs.getBoolean("files-disableinline")));
                    editor.putBoolean("chat-nocodespan", !(userInfo.prefs.has("chat-nocodespan") && userInfo.prefs.get("chat-nocodespan") instanceof Boolean && userInfo.prefs.getBoolean("chat-nocodespan")));
                    editor.putBoolean("chat-nocodeblock", !(userInfo.prefs.has("chat-nocodeblock") && userInfo.prefs.get("chat-nocodeblock") instanceof Boolean && userInfo.prefs.getBoolean("chat-nocodeblock")));
                    editor.putBoolean("chat-noquote", !(userInfo.prefs.has("chat-noquote") && userInfo.prefs.get("chat-noquote") instanceof Boolean && userInfo.prefs.getBoolean("chat-noquote")));
                    editor.putBoolean("chat-nocolor", !(userInfo.prefs.has("chat-nocolor") && userInfo.prefs.get("chat-nocolor") instanceof Boolean && userInfo.prefs.getBoolean("chat-nocolor")));
                    editor.putBoolean("inlineimages", (userInfo.prefs.has("inlineimages") && userInfo.prefs.get("inlineimages") instanceof Boolean && userInfo.prefs.getBoolean("inlineimages")));
                    if(userInfo.prefs.has("theme") && !prefs.contains("theme"))
                        editor.putString("theme", userInfo.prefs.getString("theme"));
                    if(userInfo.prefs.has("font") && !prefs.contains("monospace"))
                        editor.putBoolean("monospace", userInfo.prefs.getString("font").equals("mono"));
                    if(userInfo.prefs.has("time-left") && !prefs.contains("time-left")) {
                        editor.putBoolean("time-left", !(userInfo.prefs.has("time-left") && userInfo.prefs.get("time-left") instanceof Boolean && userInfo.prefs.getBoolean("time-left")));
                    }
                    if(userInfo.prefs.has("avatars-off") && !prefs.contains("avatars-off")) {
                        editor.putBoolean("avatars-off", !(userInfo.prefs.has("avatars-off") && userInfo.prefs.get("avatars-off") instanceof Boolean && userInfo.prefs.getBoolean("avatars-off")));
                    }
                    if(userInfo.prefs.has("chat-oneline") && !prefs.contains("chat-oneline")) {
                        editor.putBoolean("chat-oneline", !(userInfo.prefs.has("chat-oneline") && userInfo.prefs.get("chat-oneline") instanceof Boolean && userInfo.prefs.getBoolean("chat-oneline")));
                    }
                    if(userInfo.prefs.has("chat-norealname") && !prefs.contains("chat-norealname")) {
                        editor.putBoolean("chat-norealname", !(userInfo.prefs.has("chat-norealname") && userInfo.prefs.get("chat-norealname") instanceof Boolean && userInfo.prefs.getBoolean("chat-norealname")));
                    }
                    if(userInfo.prefs.has("labs") && userInfo.prefs.getJSONObject("labs").has("avatars") && !prefs.contains("avatar-images")) {
                        editor.putBoolean("avatar-images", userInfo.prefs.getJSONObject("labs").getBoolean("avatars"));
                    }
                    if(userInfo.prefs.has("hiddenMembers") && !prefs.contains("hiddenMembers")) {
                        editor.putBoolean("hiddenMembers", !(userInfo.prefs.has("hiddenMembers") && userInfo.prefs.get("hiddenMembers") instanceof Boolean && userInfo.prefs.getBoolean("hiddenMembers")));
                    }
                } else {
                    editor.putBoolean("time-24hr", false);
                    editor.putBoolean("time-seconds", false);
                    editor.putBoolean("time-left", true);
                    editor.putBoolean("avatars-off", true);
                    editor.putBoolean("chat-oneline", true);
                    editor.putBoolean("chat-norealname", true);
                    editor.putBoolean("mode-showsymbol", false);
                    editor.putBoolean("nick-colors", false);
                    editor.putBoolean("mention-colors", false);
                    editor.putBoolean("emoji-disableconvert", true);
                    editor.putBoolean("pastebin-disableprompt", true);
                    editor.putBoolean("ascii-compact", false);
                    editor.putBoolean("emoji-nobig", true);
                    editor.putBoolean("files-disableinline", true);
                    editor.putBoolean("chat-nocodespan", true);
                    editor.putBoolean("chat-nocodeblock", true);
                    editor.putBoolean("chat-noquote", true);
                    editor.putBoolean("chat-nocolor", true);
                    editor.putBoolean("inlineimages", false);
                    editor.putBoolean("avatar-images", false);
                }
                editor.apply();
                mEvents.clearCaches();
                notifyHandlers(EVENT_USERINFO, userInfo);
            }
        });

        put("set_ignores", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                if(mServers.getServer(object.cid()) != null)
                    mServers.getServer(object.cid()).updateIgnores(object.getJsonNode("masks"));
                if (!backlog)
                    notifyHandlers(EVENT_SETIGNORES, object);
            }
        });
        put("ignore_list", get("set_ignores"));

        put("heartbeat_echo", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Iterator<Entry<String, JsonNode>> iterator = object.getJsonNode("seenEids").fields();
                while (iterator.hasNext()) {
                    Map.Entry<String, JsonNode> entry = iterator.next();
                    JsonNode eids = entry.getValue();
                    Iterator<Map.Entry<String, JsonNode>> j = eids.fields();
                    while (j.hasNext()) {
                        Map.Entry<String, JsonNode> eidentry = j.next();
                        int bid = Integer.valueOf(eidentry.getKey());
                        long eid = eidentry.getValue().asLong();
                        NotificationsList.getInstance().updateLastSeenEid(bid, eid);
                        Buffer b = mBuffers.getBuffer(bid);
                        if (b != null) {
                            b.setLast_seen_eid(eid);
                            if (mEvents.lastEidForBuffer(bid) <= eid) {
                                b.setUnread(0);
                                b.setHighlights(0);
                            }
                        }
                    }
                }
                NotificationsList.getInstance().deleteOldNotifications();
                if (!backlog) {
                    notifyHandlers(EVENT_HEARTBEATECHO, object);
                }
            }
        });

        //Backlog
        put("oob_include", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                try {
                    ready = false;
                    mBuffers.invalidate();
                    mChannels.invalidate();
                    OOBFetcher t = new OOBFetcher(new URL(object.getString("api_host") + object.getString("url")), -1);
                    synchronized (oobTasks) {
                        oobTasks.put(-1, t);
                    }
                    t.connect();
                } catch (MalformedURLException e) {
                    // TODO Auto-generated catch block
                    printStackTraceToCrashlytics(e);
                }
            }
        });

        put("oob_timeout", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                IRCCloudLog.Log(Log.WARN, "IRCCloud", "OOB timed out");
                highest_eid = 0;
                streamId = null;
                ready = false;
                if (client != null)
                    client.disconnect();
            }
        });

        put("backlog_starts", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                numbuffers = object.getInt("numbuffers");
                totalbuffers = 0;
                currentBid = -1;
                notifyHandlers(EVENT_BACKLOG_START, null);
            }
        });

        put("oob_skipped", new Parser() {
                    @Override
                    public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                        Log.d("IRCCloud", "OOB was skipped");
                        ready = true;
                    }
                });

        put("backlog_cache_init", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.deleteEventsBeforeEid(object.bid(), object.eid());
            }
        });

        put("backlog_complete", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                accrued = 0;
                try {
                    Log.d("IRCCloud", "Cleaning up invalid BIDs");
                    mBuffers.dirty = true;
                    mBuffers.purgeInvalidBIDs();
                    mChannels.purgeInvalidChannels();
                    NotificationsList.getInstance().deleteOldNotifications();
                    NotificationsList.getInstance().pruneNotificationChannels();
                    mRecentConversations.prune();
                    mRecentConversations.publishShortcuts();
                    process_pending_edits(false);
                } catch (Exception e) {
                    printStackTraceToCrashlytics(e);
                }
                if (userInfo != null && userInfo.connections > 0 && (mServers.count() == 0 || mBuffers.count() == 0)) {
                    Log.e("IRCCloud", "Failed to load buffers list, reconnecting");
                    notifyHandlers(EVENT_BACKLOG_FAILED, null);
                    streamId = null;
                    if (client != null)
                        client.disconnect();
                } else {
                    failCount = 0;
                    ready = true;
                    notifyHandlers(EVENT_BACKLOG_END, null);
                    if(notifier)
                        save(1000);
                }
            }
        });

        //Misc. popup alerts
        put("bad_channel_key", new BroadcastParser(EVENT_BADCHANNELKEY));
        final Parser alert = new BroadcastParser(EVENT_ALERT);
        String[] alerts = {"too_many_channels", "no_such_channel", "bad_channel_name",
                "no_such_nick", "invalid_nick_change", "chan_privs_needed",
                "accept_exists", "banned_from_channel", "oper_only",
                "no_nick_change", "no_messages_from_non_registered", "not_registered",
                "already_registered", "too_many_targets", "no_such_server",
                "unknown_command", "help_not_found", "accept_full",
                "accept_not", "nick_collision", "nick_too_fast", "need_registered_nick",
                "save_nick", "unknown_mode", "user_not_in_channel",
                "need_more_params", "users_dont_match", "users_disabled",
                "invalid_operator_password", "flood_warning", "privs_needed",
                "operator_fail", "not_on_channel", "ban_on_chan",
                "cannot_send_to_chan", "user_on_channel", "no_nick_given",
                "no_text_to_send", "no_origin", "only_servers_can_change_mode",
                "silence", "no_channel_topic", "invite_only_chan", "channel_full", "channel_key_set",
                "blocked_channel","unknown_error","channame_in_use","pong",
                "monitor_full","mlock_restricted","cannot_do_cmd","secure_only_chan",
                "cannot_change_chan_mode","knock_delivered","too_many_knocks",
                "chan_open","knock_on_chan","knock_disabled","cannotknock","ownmode",
                "nossl","redirect_error","invalid_flood","join_flood","metadata_limit",
                "metadata_targetinvalid","metadata_nomatchingkey","metadata_keyinvalid",
                "metadata_keynotset","metadata_keynopermission","metadata_toomanysubs", "invalid_nick"};
        for (String event : alerts) {
            put(event, alert);
        }

        //Server events
        put("makeserver", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                String away = object.getString("away");
                if (getUserInfo() != null && getUserInfo().auto_away && away != null && away.equals("Auto-away"))
                    away = "";

                Server server = mServers.createServer(object.cid(), object.getString("name"), object.getString("hostname"),
                        object.getInt("port"), object.getString("nick"), object.getString("status"), object.getBoolean("ssl") ? 1 : 0,
                        object.getString("realname"), object.getString("server_pass"), object.getString("nickserv_pass"), object.getString("join_commands"),
                        object.getJsonObject("fail_info"), away, object.getJsonNode("ignores"), (object.has("order") && !object.getString("order").equals("undefined")) ? object.getInt("order") : 0, object.getString("server_realname"),
                        object.getString("ircserver"), (object.has("orgid") && !object.getString("orgid").equals("undefined")) ? object.getInt("orgid") : 0,
                        (object.has("avatars_supported") && !object.getString("avatars_supported").equals("undefined")) ? object.getInt("avatars_supported") : 0,
                        (object.has("slack") && !object.getString("slack").equals("undefined")) ? object.getInt("slack") : 0);

                if(object.has("usermask") && object.getString("usermask").length() > 0)
                    server.setUsermask(object.getString("usermask"));

                if(object.has("deferred_archives"))
                    server.deferred_archives = object.getInt("deferred_archives");

                if(object.has("avatar"))
                    server.setAvatar(object.getString("avatar"));

                if(object.has("avatar_url"))
                    server.setAvatarURL(object.getString("avatar_url"));

                String avatar_url = null;
                if(PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("avatar-images", false)) {
                    Event e = new Event();
                    e.cid = server.getCid();
                    e.from = server.getNick();
                    e.type = "buffer_msg";
                    e.hostmask = server.getUsermask();
                    e.avatar = server.getAvatar();
                    e.avatar_url = server.getAvatarURL();

                    avatar_url = e.getAvatarURL(AvatarsList.SHORTCUT_ICON_SIZE());
                }

                NotificationsList.getInstance().updateServerNick(object.cid(), object.getString("nick"), avatar_url, server.isSlack());
                NotificationsList.getInstance().addNotificationGroup(server.getCid(), server.getName() != null && server.getName().length() > 0 ? server.getName() : server.getHostname());

                mBuffers.dirty = true;

                if (!backlog) {
                    notifyHandlers(EVENT_MAKESERVER, server);
                }
            }
        });
        put("server_details_changed", get("makeserver"));
        put("connection_deleted", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mServers.deleteAllDataForServer(object.cid());
                NotificationsList.getInstance().pruneNotificationChannels();
                mRecentConversations.prune();
                if (!backlog)
                    notifyHandlers(EVENT_CONNECTIONDELETED, object.cid());
            }
        });
        put("status_changed", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Server s = mServers.getServer(object.cid());
                if(s != null) {
                    s.setStatus(object.getString("new_status"));
                    s.setFail_info(object.getJsonObject("fail_info"));
                }
                mBuffers.dirty = true;
                if (!backlog) {
                    if (object.getString("new_status").equals("disconnected")) {
                        ArrayList<Buffer> buffers = mBuffers.getBuffersForServer(object.cid());
                        for (Buffer b : buffers) {
                            mChannels.deleteChannel(b.getBid());
                        }
                    }
                    notifyHandlers(EVENT_STATUSCHANGED, object);
                }
            }
        });
        put("isupport_params", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Server s = mServers.getServer(object.cid());
                if(s != null) {
                    s.updateIsupport(object.getJsonObject("params"));
                    s.updateUserModes(object.getString("usermodes"));
                }
            }
        });
        put("reorder_connections", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                JsonNode order = object.getJsonNode("order");
                for (int i = 0; i < order.size(); i++) {
                    mServers.getServer(order.get(i).asInt()).setOrder(i + 1);
                }
                notifyHandlers(EVENT_REORDERCONNECTIONS, object);
            }
        });

        //Buffer events
        put("open_buffer", new BroadcastParser(EVENT_OPENBUFFER));
        put("makebuffer", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer buffer = mBuffers.createBuffer(object.bid(), object.cid(),
                        (object.has("min_eid") && !object.getString("min_eid").equalsIgnoreCase("undefined")) ? object.getLong("min_eid") : 0,
                        (object.has("last_seen_eid") && !object.getString("last_seen_eid").equalsIgnoreCase("undefined")) ? object.getLong("last_seen_eid") : -1, object.getString("name"), object.getString("buffer_type"),
                        (object.has("archived") && object.getBoolean("archived")) ? 1 : 0, (object.has("deferred") && object.getBoolean("deferred")) ? 1 : 0, (object.has("timeout") && object.getBoolean("timeout")) ? 1 : 0, object.getLong("created"));
                if(buffer.getTimeout() == 1 || buffer.getDeferred() == 1)
                    mEvents.deleteEventsForBuffer(buffer.getBid());
                NotificationsList.getInstance().updateLastSeenEid(buffer.getBid(), buffer.getLast_seen_eid());
                if(buffer.getTimeout() == 1 || buffer.getDeferred() == 1)
                    mEvents.deleteEventsForBuffer(buffer.getBid());
                if(buffer.getServer() != null && buffer.getArchived() == 1)
                    buffer.getServer().deferred_archives--;
                if (!backlog) {
                    notifyHandlers(EVENT_MAKEBUFFER, buffer);
                }
                totalbuffers++;
            }
        });
        put("delete_buffer", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mBuffers.deleteAllDataForBuffer(object.bid());
                NotificationsList.getInstance().deleteNotificationsForBid(object.bid());
                NotificationsList.getInstance().pruneNotificationChannels();
                mRecentConversations.prune();
                if (!backlog)
                    notifyHandlers(EVENT_DELETEBUFFER, object.bid());
            }
        });
        put("buffer_archived", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer b = mBuffers.getBuffer(object.bid());
                if(b != null)
                    b.setArchived(1);
                if (!backlog) {
                    mRecentConversations.prune();
                    notifyHandlers(EVENT_BUFFERARCHIVED, object.bid());
                }
            }
        });
        put("buffer_unarchived", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer b = mBuffers.getBuffer(object.bid());
                if(b != null)
                    b.setArchived(0);
                Server s = mServers.getServer(object.cid());
                if(s != null && s.deferred_archives > 0)
                    s.deferred_archives--;
                if (!backlog) {
                    notifyHandlers(EVENT_BUFFERUNARCHIVED, object.bid());
                }
            }
        });
        put("rename_conversation", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer b = mBuffers.getBuffer(object.bid());
                if(b != null)
                    b.setName(object.getString("new_name"));
                if (!backlog) {
                    notifyHandlers(EVENT_RENAMECONVERSATION, object.bid());
                }
            }
        });
        put("rename_channel", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer b = mBuffers.getBuffer(object.bid());
                if(b != null)
                    b.setName(object.getString("new_name"));
                Channel c = mChannels.getChannelForBuffer(object.bid());
                if(c != null)
                    c.name = object.getString("new_name");
                if (!backlog) {
                    notifyHandlers(EVENT_RENAMECONVERSATION, object.bid());
                }
            }
        });
        Parser msg = new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                boolean newEvent = (mEvents.getEvent(object.eid(), object.bid()) == null);
                Event event = mEvents.addEvent(object);
                Buffer b = mBuffers.getBuffer(object.bid());

                if(event.eid == -1) {
                    alert.parse(object, backlog);
                } else {
                    if (b != null && event.isImportant(b.getType())) {
                        if(event.from != null) {
                            User u = mUsers.getUser(event.bid, event.from);
                            if (u != null) {
                                if(u.last_message < event.eid)
                                    u.last_message = event.eid;
                                if(event.highlight && u.last_mention < event.eid)
                                    u.last_mention = event.eid;
                            }
                        }

                        if (event.eid > b.getLast_seen_eid()) {
                            if ((event.highlight || b.isConversation())) {
                                if (newEvent) {
                                    b.setHighlights(b.getHighlights() + 1);
                                    b.setUnread(1);
                                }
                                if (!backlog) {
                                    JSONObject bufferDisabledMap = null;
                                    boolean show = true;
                                    String pref_type = b.isChannel() ? "channel" : "buffer";
                                    if (userInfo != null && userInfo.prefs != null && userInfo.prefs.has(pref_type + "-disableTrackUnread")) {
                                        bufferDisabledMap = userInfo.prefs.getJSONObject(pref_type + "-disableTrackUnread");
                                        if (bufferDisabledMap != null && bufferDisabledMap.has(String.valueOf(b.getBid())) && bufferDisabledMap.getBoolean(String.valueOf(b.getBid())))
                                            show = false;
                                    }
                                    if (userInfo != null && userInfo.prefs != null && userInfo.prefs.has(pref_type + "-notifications-mute")) {
                                        JSONObject bufferMutedMap = userInfo.prefs.getJSONObject(pref_type + "-notifications-mute");
                                        if (bufferMutedMap != null && bufferMutedMap.has(String.valueOf(b.getBid())) && bufferMutedMap.getBoolean(String.valueOf(b.getBid())))
                                            show = false;
                                    }
                                    try {
                                        if (IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).getString("gcm_token", "").length() > 0)
                                            show = false;
                                    } catch (Exception e) {
                                        show = true;
                                    }
                                    if (show && NotificationsList.getInstance().getNotification(event.eid) == null) {
                                        String message = ColorFormatter.strip(event.msg).toString();
                                        String server_name = b.getServer().getName();
                                        if (server_name == null || server_name.length() == 0)
                                            server_name = b.getServer().getHostname();
                                        String from = event.nick;
                                        if (from == null)
                                            from = (event.from != null) ? event.from : event.server;

                                        NotificationsList.getInstance().addNotification(event.cid, event.bid, event.eid, (event.nick != null) ? event.nick : event.from, message, b.getName(), b.getType(), event.type, server_name, event.getAvatarURL(512));
                                        switch (b.getType()) {
                                            case "conversation":
                                                if (event.type.equals("buffer_me_msg"))
                                                    NotificationsList.getInstance().showNotifications("— " + b.getName() + " " + message);
                                                else
                                                    NotificationsList.getInstance().showNotifications(b.getName() + ": " + message);
                                                break;
                                            case "console":
                                                if (event.from == null || event.from.length() == 0) {
                                                    Server s = mServers.getServer(event.cid);
                                                    if (s.getName() != null && s.getName().length() > 0)
                                                        NotificationsList.getInstance().showNotifications(s.getName() + ": " + message);
                                                    else
                                                        NotificationsList.getInstance().showNotifications(s.getHostname() + ": " + message);
                                                } else {
                                                    NotificationsList.getInstance().showNotifications(event.from + ": " + message);
                                                }
                                                break;
                                            default:
                                                if (event.type.equals("buffer_me_msg"))
                                                    NotificationsList.getInstance().showNotifications(b.getName() + ": — " + event.nick + " " + message);
                                                else
                                                    NotificationsList.getInstance().showNotifications(b.getName() + ": <" + event.from + "> " + message);
                                                break;
                                        }
                                    }
                                }
                            } else {
                                b.setUnread(1);
                            }
                        }
                    }
                }

                if (handlers.size() == 0 && b != null && !b.getScrolledUp() && mEvents.getSizeOfBuffer(b.getBid()) > 200)
                    mEvents.pruneEvents(b.getBid());

                if (event.reqid >= 0) {
                    Event pending = mEvents.findPendingEventForReqid(event.bid, event.reqid);
                    if(pending != null) {
                        try {
                            if (pending.expiration_timer != null)
                                pending.expiration_timer.cancel();
                        } catch (Exception e1) {
                            //Timer already cancelled
                        }
                        if(pending.eid != event.eid)
                            mEvents.deleteEvent(pending.eid, pending.bid);
                    }
                } else if(event.self && b != null && b.isConversation()) {
                    mEvents.clearPendingEvents(event.bid);
                }

                if(event.isMessage()) {
                    String avatar = event.getAvatarURL(AvatarsList.SHORTCUT_ICON_SIZE());
                    AvatarsList.setAvatarURL(event.cid, event.type.equals("buffer_me_msg")?event.nick:event.from_nick, event.eid, avatar);
                    if (b != null && b.isConversation() && b.getName().equalsIgnoreCase(event.from)) {
                        try {
                            if (avatar != null && PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("avatar-images", true)) {
                                URL url = new URL(event.getAvatarURL(AvatarsList.SHORTCUT_ICON_SIZE()));
                                if (!ImageList.getInstance().cacheFile(url).exists()) {
                                    ImageList.getInstance().fetchImage(url, null);
                                }
                            }
                        } catch (Exception e) {
                        }
                    }
                }

                if(event.self) {
                    if(b != null)
                        mRecentConversations.updateConversation(event.cid, event.bid, b.getName(), b.getType(), event.eid / 1000);
                    if(!backlog)
                        mRecentConversations.publishShortcuts();
                }

                if (!backlog) {
                    notifyHandlers(EVENT_BUFFERMSG, event);
                }
            }
        };

        String[] msgs = { "buffer_msg", "buffer_me_msg", "wait", "banned", "kill", "connecting_cancelled",
                "target_callerid", "notice", "server_motdstart", "server_welcome", "server_motd", "server_endofmotd",
                "server_nomotd", "server_luserclient", "server_luserop", "server_luserconns", "server_luserme", "server_n_local",
                "server_luserchannels", "server_n_global", "server_yourhost","server_created", "server_luserunknown",
                "services_down", "your_unique_id", "callerid", "target_notified", "myinfo", "hidden_host_set", "unhandled_line",
                "unparsed_line", "connecting_failed", "nickname_in_use", "channel_invite", "motd_response", "socket_closed",
                "channel_mode_list_change", "msg_services", "stats", "statslinkinfo", "statscommands", "statscline",
                "statsnline", "statsiline", "statskline", "statsqline", "statsyline", "statsbline", "statsgline", "statstline",
                "statseline", "statsvline", "statslline", "statsuptime", "statsoline", "statshline", "statssline", "statsuline",
                "statsdebug", "spamfilter", "endofstats", "inviting_to_channel", "error", "too_fast", "no_bots",
                "wallops", "logged_in_as", "sasl_fail", "sasl_too_long", "sasl_aborted", "sasl_already",
                "you_are_operator", "btn_metadata_set", "sasl_success", "version", "channel_name_change",
                "cap_ls", "cap_list", "cap_new", "cap_del", "cap_req","cap_ack","cap_nak","cap_raw","cap_invalid", "help",
                "newsflash", "invited", "server_snomask", "codepage", "logged_out", "nick_locked", "info_response", "generic_server_info",
                "unknown_umode", "bad_ping", "cap_raw", "rehashed_config", "knock", "bad_channel_mask", "kill_deny",
                "chan_own_priv_needed", "not_for_halfops", "chan_forbidden", "starircd_welcome", "zurna_motd",
                "ambiguous_error_message", "list_usage", "list_syntax", "who_syntax", "text", "admin_info",
                "sqline_nick", "user_chghost", "loaded_module", "unloaded_module", "invite_notify" };
        for (String event : msgs) {
            put(event, msg);
        }

        //Channel events
        put("link_channel", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    notifyHandlers(EVENT_LINKCHANNEL, object);
                }
            }
        });
        put("channel_init", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                JsonNode topic = object.getJsonObject("topic");
                String set_by = "";
                if(topic != null && topic.has("nick") && !topic.get("nick").isNull())
                    set_by = topic.get("nick").asText();
                else if(topic != null && topic.has("server") && !topic.get("server").isNull())
                    set_by = topic.get("server").asText();
                Channel channel = mChannels.createChannel(object.cid(), object.bid(), object.getString("chan"),
                        (topic == null || topic.get("text").isNull()) ? "" : topic.get("text").asText(),
                        (topic == null || !topic.has("time")) ? 0 : topic.get("time").asLong(),
                        set_by, object.getString("channel_type"),
                        object.getLong("timestamp"));
                mChannels.updateMode(object.bid(), object.getString("mode"), object.getJsonObject("ops"), true);
                mChannels.updateURL(object.bid(), object.getString("url"));
                mUsers.deleteUsersForBuffer(object.bid());
                JsonNode users = object.getJsonNode("members");
                if(users != null) {
                    Iterator<JsonNode> iterator = users.elements();
                    while (iterator.hasNext()) {
                        JsonNode user = iterator.next();
                        mUsers.createUser(object.cid(), object.bid(), user.get("nick").asText(), user.get("usermask").asText(), user.has("mode") ? user.get("mode").asText() : "", user.has("ircserver") ? user.get("ircserver").asText() : "", (user.has("away") && user.get("away").asBoolean()) ? 1 : 0, user.hasNonNull("display_name") ? user.get("display_name").asText() : null, false);
                    }
                }
                mBuffers.dirty = true;
                if (!backlog)
                    notifyHandlers(EVENT_CHANNELINIT, channel);
            }
        });
        put("channel_topic", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    mChannels.updateTopic(object.bid(), object.getString("topic"), object.getLong("eid") / 1000000, object.has("author") ? object.getString("author") : object.getString("server"));
                    notifyHandlers(EVENT_CHANNELTOPIC, object);
                }
            }
        });
        put("channel_url", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mChannels.updateURL(object.bid(), object.getString("url"));
            }
        });
        put("channel_mode", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    mChannels.updateMode(object.bid(), object.getString("newmode"), object.getJsonObject("ops"), false);
                    notifyHandlers(EVENT_CHANNELMODE, object);
                }
            }
        });
        put("channel_mode_is", get("channel_mode"));
        put("channel_timestamp", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                if (!backlog) {
                    mChannels.updateTimestamp(object.bid(), object.getLong("timestamp"));
                    notifyHandlers(EVENT_CHANNELTIMESTAMP, object);
                }
            }
        });
        put("joined_channel", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if(object.type().startsWith("you_"))
                    mBuffers.dirty = true;
                if (!backlog) {
                    mUsers.createUser(object.cid(), object.bid(), object.getString("nick"), object.getString("hostmask"), "", object.getString("ircserver"), 0, object.getString("display_name"));
                    notifyHandlers(EVENT_JOIN, object);
                }
            }
        });
        put("you_joined_channel", get("joined_channel"));
        put("parted_channel", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if(object.type().startsWith("you_"))
                    mBuffers.dirty = true;
                if (!backlog) {
                    mUsers.deleteUser(object.bid(), object.getString("nick"));
                    if (object.type().equals("you_parted_channel")) {
                        mChannels.deleteChannel(object.bid());
                        mUsers.deleteUsersForBuffer(object.bid());
                    }
                    notifyHandlers(EVENT_PART, object);
                }
            }
        });
        put("you_parted_channel", get("parted_channel"));
        put("quit", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    if(object.type().startsWith("you_"))
                        mBuffers.dirty = true;
                    mUsers.deleteUser(object.bid(), object.getString("nick"));
                    notifyHandlers(EVENT_QUIT, object);
                }
            }
        });
        put("quit_server", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    mBuffers.dirty = true;
                    notifyHandlers(EVENT_QUIT, object);
                }
            }
        });
        put("kicked_channel", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    mUsers.deleteUser(object.bid(), object.getString("nick"));
                    if (object.type().equals("you_kicked_channel")) {
                        mChannels.deleteChannel(object.bid());
                        mUsers.deleteUsersForBuffer(object.bid());
                        mBuffers.dirty = true;
                    }
                    notifyHandlers(EVENT_KICK, object);
                }
            }
        });
        put("you_kicked_channel", get("kicked_channel"));

        //Member list events
        put("nickchange", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    mUsers.updateNick(object.bid(), object.getString("oldnick"), object.getString("newnick"));
                    if (object.type().equals("you_nickchange")) {
                        Server s = mServers.getServer(object.cid());
                        if(s != null)
                            s.setNick(object.getString("newnick"));
                        NotificationsList.getInstance().updateServerNick(object.cid(), object.getString("newnick"), s != null ? s.getAvatarURL() : null, s != null && s.isSlack());
                    }
                    notifyHandlers(EVENT_NICKCHANGE, object);
                }
            }
        });
        put("you_nickchange", get("nickchange"));
        put("display_name_change", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                if (!backlog) {
                    mUsers.updateDisplayName(object.cid(), object.getString("nick"), object.getString("display_name"));
                    notifyHandlers(EVENT_DISPLAYNAMECHANGE, object);
                }
            }
        });
        put("user_channel_mode", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    mUsers.updateMode(object.bid(), object.getString("nick"), object.getString("newmode"));
                    notifyHandlers(EVENT_USERCHANNELMODE, object);
                }
            }
        });
        put("member_updates", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                JsonNode updates = object.getJsonObject("updates");
                Iterator<Map.Entry<String, JsonNode>> i = updates.fields();
                while (i.hasNext()) {
                    Map.Entry<String, JsonNode> e = i.next();
                    JsonNode user = e.getValue();
                    mUsers.updateAway(object.cid(), user.get("nick").asText(), user.get("away").asBoolean() ? 1 : 0);
                    mUsers.updateHostmask(object.bid(), user.get("nick").asText(), user.get("usermask").asText());
                }
                if (!backlog)
                    notifyHandlers(EVENT_MEMBERUPDATES, null);
            }
        });
        put("user_away", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer b = mBuffers.getBuffer(object.bid());
                if(b != null && b.isConsole()) {
                    mUsers.updateAwayMsg(object.cid(), object.getString("nick"), 1, object.getString("msg"));
                    b = mBuffers.getBufferByName(object.cid(), object.getString("nick"));
                    if (b != null)
                        b.setAway_msg(object.getString("msg"));
                    if (!backlog) {
                        notifyHandlers(EVENT_AWAY, object);
                    }
                }
            }
        });
        put("away", get("user_away"));
        put("user_back", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer b = mBuffers.getBuffer(object.bid());
                if(b != null && b.isConsole()) {
                    mUsers.updateAwayMsg(object.cid(), object.getString("nick"), 0, "");
                    b = mBuffers.getBufferByName(object.cid(), object.getString("nick"));
                    if (b != null)
                        b.setAway_msg("");
                    if (!backlog) {
                        notifyHandlers(EVENT_AWAY, object);
                    }
                }
            }
        });
        put("self_away", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                if (!backlog) {
                    mUsers.updateAwayMsg(object.cid(), object.getString("nick"), 1, object.getString("away_msg"));
                    if(mServers.getServer(object.cid()) != null)
                        mServers.getServer(object.cid()).setAway(object.getString("away_msg"));
                    notifyHandlers(EVENT_AWAY, object);
                }
            }
        });
        put("self_back", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mUsers.updateAwayMsg(object.cid(), object.getString("nick"), 0, "");
                if(mServers.getServer(object.cid()) != null)
                    mServers.getServer(object.cid()).setAway("");
                if (!backlog)
                    notifyHandlers(EVENT_SELFBACK, object);
            }
        });
        put("self_details", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Server s = mServers.getServer(object.cid());
                if(s != null) {
                    s.setUsermask(object.getString("usermask"));
                    if(object.has("server_realname"))
                        s.setServerRealname(object.getString("server_realname"));
                }
                Event e = mEvents.addEvent(object);
                if (!backlog) {
                    notifyHandlers(EVENT_SELFDETAILS, e);
                    e = mEvents.getEvent(e.eid + 1, e.bid);
                    if(e != null)
                        notifyHandlers(EVENT_SELFDETAILS, e);
                }
            }
        });
        put("user_mode", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                mEvents.addEvent(object);
                if (!backlog) {
                    Server s = mServers.getServer(object.cid());
                    if(s != null)
                        s.setMode(object.getString("newmode"));
                    notifyHandlers(EVENT_USERMODE, object);
                }
            }
        });

        //Message updates
        put("empty_msg", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                pendingEdits.add(object);
                process_pending_edits(backlog);
            }
        });

        //Various lists
        put("ban_list", new BroadcastParser(EVENT_BANLIST));
        put("accept_list", new BroadcastParser(EVENT_ACCEPTLIST));
        put("names_reply", new BroadcastParser(EVENT_NAMESLIST));
        put("whois_response", new BroadcastParser(EVENT_WHOIS));
        put("list_response_fetching", new BroadcastParser(EVENT_LISTRESPONSEFETCHING));
        put("list_response_toomany", new BroadcastParser(EVENT_LISTRESPONSETOOMANY));
        put("list_response", new BroadcastParser(EVENT_LISTRESPONSE));
        put("map_list", new BroadcastParser(EVENT_SERVERMAPLIST));
        put("quiet_list", new BroadcastParser(EVENT_QUIETLIST));
        put("ban_exception_list", new BroadcastParser(EVENT_BANEXCEPTIONLIST));
        put("invite_list", new BroadcastParser(EVENT_INVITELIST));
        put("chanfilter_list", new BroadcastParser(EVENT_CHANFILTERLIST));
        put("channel_query", new BroadcastParser(EVENT_CHANNELQUERY));
        put("text", new BroadcastParser(EVENT_TEXTLIST));
        put("who_special_response", new BroadcastParser(EVENT_WHOSPECIALRESPONSE));
        put("modules_list", new BroadcastParser(EVENT_MODULESLIST));
        put("links_response", new BroadcastParser(EVENT_LINKSRESPONSE));
        put("whowas_response", new BroadcastParser(EVENT_WHOWAS));
        put("trace_response", new BroadcastParser(EVENT_TRACERESPONSE));
        put("export_finished", new BroadcastParser(EVENT_LOGEXPORTFINISHED));
        put("avatar_change", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                User u = mUsers.getUser(object.bid(), object.getString("nick"));
                if(u != null) {
                    Event e = new Event();
                    e.cid = object.cid();
                    e.from = object.getString("nick");
                    e.type = "buffer_msg";
                    e.hostmask = u.hostmask;
                    e.avatar = object.getString("avatar");
                    e.avatar_url = object.getString("avatar_url");

                    String avatar_url = e.getAvatarURL(AvatarsList.SHORTCUT_ICON_SIZE());
                    AvatarsList.setAvatarURL(object.cid(), object.getString("nick"), System.currentTimeMillis() * 1000L, avatar_url);
                    RecentConversationsList.getInstance().publishShortcuts();
                    NotificationsList.getInstance().showNotificationsNow();
                }
                if(object.getBoolean("self")) {
                    Server s = mServers.getServer(object.cid());
                    if (s != null) {
                        s.setAvatar(object.getString("avatar"));
                        s.setAvatarURL(object.getString("avatar_url"));

                        Event e = new Event();
                        e.cid = s.getCid();
                        e.from = s.getNick();
                        e.type = "buffer_msg";
                        e.hostmask = s.getUsermask();
                        e.avatar = s.getAvatar();
                        e.avatar_url = s.getAvatarURL();

                        String avatar_url = e.getAvatarURL(AvatarsList.SHORTCUT_ICON_SIZE());

                        NotificationsList.getInstance().updateServerNick(object.cid(), object.getString("nick"), avatar_url, s.isSlack());
                    }
                }
                notifyHandlers(EVENT_AVATARCHANGE, object);
            }
        });
        put("who_response", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                if (!backlog) {
                    Buffer b = mBuffers.getBufferByName(object.cid(), object.getString("subject"));
                    if (b != null) {
                        JsonNode users = object.getJsonNode("users");
                        for (int i = 0; i < users.size(); i++) {
                            JsonNode user = users.get(i);
                            mUsers.updateHostmask(b.getBid(), user.get("nick").asText(), user.get("usermask").asText());
                            mUsers.updateAway(b.getCid(), user.get("nick").asText(), user.get("away").asBoolean() ? 1 : 0);
                        }
                    }
                    notifyHandlers(EVENT_WHOLIST, object);
                }
            }
        });
        put("time", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Event e = mEvents.addEvent(object);
                if (!backlog) {
                    notifyHandlers(EVENT_ALERT, object);
                    notifyHandlers(EVENT_BUFFERMSG, e);
                }
            }
        });
        put("channel_topic_is", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                Buffer b = mBuffers.getBufferByName(object.cid(), object.getString("chan"));
                if (b != null) {
                    Channel c = mChannels.getChannelForBuffer(b.getBid());
                    if (c != null) {
                        c.topic_author = object.has("author") ? object.getString("author") : object.getString("server");
                        c.topic_time = object.getLong("time");
                        c.topic_text = object.getString("text");
                    }
                }
                if (!backlog)
                    notifyHandlers(EVENT_CHANNELTOPICIS, object);
            }
        });
        put("watch_status", new Parser() {
            @Override
            public void parse(IRCCloudJSONObject object, boolean backlog) throws JSONException {
                object.setEid(System.currentTimeMillis() * 1000L);
                Event e = mEvents.addEvent(object);
                if (!backlog) {
                    notifyHandlers(EVENT_WATCHSTATUS, object);
                    notifyHandlers(EVENT_BUFFERMSG, e);
                }
            }
        });
    }};

    //https://stackoverflow.com/a/11459962
    private static JsonNode mergeJsonNode(JsonNode mainNode, JsonNode updateNode) {
        Iterator<String> fieldNames = updateNode.fieldNames();
        while (fieldNames.hasNext()) {
            String fieldName = fieldNames.next();
            JsonNode jsonNode = mainNode.get(fieldName);
            // if field exists and is an embedded object
            if (jsonNode != null && jsonNode.isObject()) {
                mergeJsonNode(jsonNode, updateNode.get(fieldName));
            }
            else {
                if (mainNode instanceof ObjectNode) {
                    // Overwrite field
                    JsonNode value = updateNode.get(fieldName);
                    ((ObjectNode) mainNode).put(fieldName, value);
                }
            }
        }

        return mainNode;
    }

    private synchronized void process_pending_edits(boolean backlog) {
        ArrayList<IRCCloudJSONObject> edits = pendingEdits;
        pendingEdits = new ArrayList<>();

        for(IRCCloudJSONObject o : edits) {
            JsonNode entities = o.getJsonNode("entities");
            if(entities != null) {
                if (entities.has("delete")) {
                    Event e = mEvents.getEvent(o.bid(), entities.get("delete").asText());
                    if (e != null) {
                        mEvents.deleteEvent(e.eid, e.bid);
                        if(!backlog)
                            notifyHandlers(EVENT_MESSAGECHANGE, o);
                    } else {
                        pendingEdits.add(o);
                    }
                } else if(entities.has("edit")) {
                    Event e = mEvents.getEvent(o.bid(), entities.get("edit").asText());
                    if(e != null) {
                        if (o.eid() >= e.lastEditEID) {
                            if (entities.has("edit_text")) {
                                e.msg = TextUtils.htmlEncode(Normalizer.normalize(entities.get("edit_text").asText(), Normalizer.Form.NFC)).replace("  ", "&nbsp; ");
                                if (e.msg.startsWith(" "))
                                    e.msg = "&nbsp;" + e.msg.substring(1);
                                e.edited = true;
                                if(e.entities instanceof ObjectNode) {
                                    if(e.entities.has("mentions"))
                                        ((ObjectNode)e.entities).remove("mentions");
                                    if(e.entities.has("mention_data"))
                                        ((ObjectNode)e.entities).remove("mention_data");
                                }
                            }
                            if(e.entities != null) {
                                mergeJsonNode(e.entities, entities);
                            } else {
                                e.entities = entities;
                            }
                            e.lastEditEID = o.eid();
                            e.formatted = null;
                            e.html = null;
                            e.ready_for_display = false;
                            e.linkified = false;
                        }
                        if(!backlog)
                            notifyHandlers(EVENT_MESSAGECHANGE, o);
                    } else {
                        pendingEdits.add(o);
                    }
                }
            }
        }

        if(pendingEdits.size() > 0)
            IRCCloudLog.Log(Log.INFO, TAG, "Queued pending edits: " + pendingEdits.size());
    }

    public synchronized void parse_object(IRCCloudJSONObject object, boolean backlog) throws JSONException {
        cancel_idle_timer();
        //Log.d(TAG, "Backlog: " + backlog + " count: " + currentcount + " New event: " + object);
        if (!object.has("type")) {
            //Log.d(TAG, "Response: " + object);
            if (object.has("success") && !object.getBoolean("success") && object.has("message")) {
                IRCCloudLog.Log(Log.ERROR, TAG, "Error: " + object);
                if(object.getString("message").equals("auth")) {
                    logout();
                    notifyHandlers(EVENT_AUTH_FAILED, object);
                } else if(object.getString("message").equals("set_shard")) {
                    disconnect();
                    ready = false;
                    SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getSharedPreferences("prefs", 0).edit();
                    editor.putString("session_key", object.getString("cookie"));
                    if (object.has("websocket_path")) {
                        IRCCLOUD_PATH = object.getString("websocket_path");
                    }
                    editor.putString("path", NetworkConnection.IRCCLOUD_PATH);
                    editor.apply();
                    if (object.has("api_host")) {
                        set_api_host(object.getString("api_host"));
                    }
                    connect();
                } else if(object.getString("message").equals("temp_unavailable")) {
                    notifyHandlers(EVENT_TEMP_UNAVAILABLE, object);
                } else if(object.getString("message").equals("invalid_nick")) {
                    notifyHandlers(EVENT_ALERT, object);
                } else if(backlog) {
                    disconnect();
                    fail();
                }
            }
            if (object.has("_reqid") && resultCallbacks.containsKey(object.getInt("_reqid"))) {
                resultCallbacks.get(object.getInt("_reqid")).onIRCResult(object);
                resultCallbacks.remove(object.getInt("_reqid"));
            }
            return;
        }
        String type = object.type();
        if (type != null && type.length() > 0) {
            //notifyHandlers(EVENT_DEBUG, "Type: " + type + " BID: " + object.bid() + " EID: " + object.eid());
            //IRCCloudLog.Log("New event: " + type);
            //Log.d(TAG, "New event: " + type);
            if ((backlog || accrued > 0) && object.bid() > -1 && object.bid() != currentBid && object.eid() > 0) {
                if(!backlog) {
                    if(firstEid == -1) {
                        firstEid = object.eid();
                        if (firstEid > highest_eid) {
                            Log.w("IRCCloud", "Backlog gap detected, purging cache and reconnecting");
                            highest_eid = 0;
                            streamId = null;
                            failCount = 0;
                            if (client != null)
                                client.disconnect();
                            return;
                        }
                    }
                }
                currentBid = object.bid();
                currentcount = 0;
            }
            Parser p = parserMap.get(type);
            if (p != null) {
                p.parse(object, backlog);
            } else if (!parserMap.containsKey(type)) {
                IRCCloudLog.Log(Log.WARN, TAG, "Unhandled type: " + object.type());
                //Log.w(TAG, "Unhandled type: " + object);
            }

            if (backlog || type.equals("backlog_complete") || accrued > 0) {
                if ((object.bid() > -1 || type.equals("backlog_complete")) && !type.equals("makebuffer") && !type.equals("channel_init")) {
                    currentcount++;
                    if (object.bid() != currentBid) {
                        currentBid = object.bid();
                        currentcount = 0;
                    }
                }
            }
            if (backlog || type.equals("backlog_complete")) {
                if (numbuffers > 0 && currentcount < 100) {
                    notifyHandlers(EVENT_PROGRESS, ((totalbuffers + ((float) currentcount / (float) 100)) / numbuffers) * 1000.0f);
                }
            } else if (accrued > 0) {
                notifyHandlers(EVENT_PROGRESS, ((float) currentcount++ / (float) accrued) * 1000.0f);
            }
            if((!backlog || numbuffers > 0) && object.eid() > highest_eid) {
                highest_eid = object.eid();
                /*if(!backlog) {
                    SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit();
                    editor.putLong("highest_eid", highest_eid);
                    editor.apply();
                }*/
            }
        }

        if (!backlog && idle_interval > 0 && accrued < 1)
            schedule_idle_timer();
    }

    public boolean isWifi() {
        ConnectivityManager cm = (ConnectivityManager) IRCCloudApplication.getInstance().getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo ni = cm.getActiveNetworkInfo();
        return ni != null && ni.getType() == ConnectivityManager.TYPE_WIFI;
    }

    public String fetch(URL url, String postdata, String sk, String token, HashMap<String, String>headers) throws Exception {
        if(BuildConfig.MOCK_DATA)
            return null;

        HttpURLConnection conn = null;

        Proxy proxy = null;
        int port = -1;

        String host = System.getProperty("http.proxyHost", null);
        try {
            port = Integer.parseInt(System.getProperty("http.proxyPort", "8080"));
        } catch (NumberFormatException e) {
            port = -1;
        }

        if (host != null && host.length() > 0 && !host.equalsIgnoreCase("localhost") && !host.equalsIgnoreCase("127.0.0.1") && port > 0) {
            InetSocketAddress proxyAddr = new InetSocketAddress(host, port);
            proxy = new Proxy(Proxy.Type.HTTP, proxyAddr);
        }

        if (host != null && host.length() > 0 && !host.equalsIgnoreCase("localhost") && !host.equalsIgnoreCase("127.0.0.1") && port > 0) {
            IRCCloudLog.Log(Log.DEBUG, TAG, "Requesting via proxy: " + host);
        }

        HttpMetric metric = null;
        try {
            metric = FirebasePerformance.getInstance().newHttpMetric(url, postdata != null ? FirebasePerformance.HttpMethod.POST : FirebasePerformance.HttpMethod.GET);
        } catch (Exception e) {

        }
        if (url.getProtocol().toLowerCase().equals("https")) {
            HttpsURLConnection https = (HttpsURLConnection) ((proxy != null) ? url.openConnection(proxy) : url.openConnection(Proxy.NO_PROXY));
            https.setSSLSocketFactory(TrustKit.getInstance().getSSLSocketFactory(url.getHost()));
            conn = https;
        } else {
            conn = (HttpURLConnection) ((proxy != null) ? url.openConnection(proxy) : url.openConnection(Proxy.NO_PROXY));
        }

        conn.setReadTimeout(30000);
        conn.setConnectTimeout(30000);
        conn.setUseCaches(false);
        conn.setRequestProperty("User-Agent", useragent);
        conn.setRequestProperty("Accept", "application/json");
        if(headers != null) {
            for (String key : headers.keySet()) {
                conn.setRequestProperty(key, headers.get(key));
            }
        }
        if (sk != null && url.getProtocol().equals("https") && url.getUserInfo() == null && (url.getHost().equals(IRCCLOUD_HOST) || (!BuildConfig.ENTERPRISE && url.getHost().endsWith(".irccloud.com"))))
            conn.setRequestProperty("Cookie", "session=" + sk);
        if (token != null)
            conn.setRequestProperty("x-auth-formtoken", token);
        if (postdata != null) {
            conn.setRequestMethod("POST");
            conn.setDoOutput(true);
            conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded");
            OutputStream ostr = null;
            try {
                ostr = conn.getOutputStream();
                ostr.write(postdata.getBytes());
            } catch (Exception e) {
                printStackTraceToCrashlytics(e);
            } finally {
                if (ostr != null)
                    ostr.close();
            }
            if(metric != null)
                metric.setRequestPayloadSize(postdata.length());
        }
        InputStream is = null;
        String response = "";

        try {
            ConnectivityManager cm = (ConnectivityManager) IRCCloudApplication.getInstance().getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo ni = cm.getActiveNetworkInfo();
            if (ni != null && ni.getType() == ConnectivityManager.TYPE_WIFI) {
                IRCCloudLog.Log(Log.DEBUG, TAG, "Loading via WiFi");
            } else {
                IRCCloudLog.Log(Log.DEBUG, TAG, "Loading via mobile");
            }
        } catch (Exception e) {
        }

        if(metric != null)
            metric.start();

        try {
            if (conn.getInputStream() != null) {
                is = conn.getInputStream();
            }
        } catch (IOException e) {
            if (conn.getErrorStream() != null) {
                is = conn.getErrorStream();
            } else {
                printStackTraceToCrashlytics(e);
            }
        }

        if (is != null) {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            byte[] buffer = new byte[8192];
            int len;
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            response = os.toString("UTF-8");
        }
        if(metric != null) {
            metric.setResponsePayloadSize(response.length());
            metric.setHttpResponseCode(conn.getResponseCode());
            metric.setResponseContentType(conn.getContentType());
            metric.stop();
        }
        conn.disconnect();
        return response;
    }

    public void addHandler(IRCEventHandler handler) {
        synchronized (handlers) {
            if (!handlers.contains(handler))
                handlers.add(handler);
            if (saveTimerTask != null)
                saveTimerTask.cancel();
            saveTimerTask = null;
        }
    }

    @TargetApi(24)
    public void removeHandler(IRCEventHandler handler) {
        synchronized (handlers) {
            handlers.remove(handler);
        }
    }

    public boolean uploadsAvailable() {
        return userInfo != null && !userInfo.uploads_disabled;
    }

    public boolean isVisible() {
        return (handlers != null && handlers.size() > 0);
    }

    public void notifyHandlers(int message, Object object) {
        notifyHandlers(message, object, null);
    }

    public void notifyHandlers(int message, final Object object, IRCEventHandler exclude) {
        synchronized (handlers) {
            int bid;

            switch(message) {
                case EVENT_OOB_START:
                    numbuffers = 0;
                    totalbuffers = 0;
                    currentBid = -1;
                    break;
                case EVENT_OOB_END:
                    bid = ((OOBFetcher)object).getBid();
                    ArrayList<Buffer> buffers = mBuffers.getBuffers();
                    for (Buffer b : buffers) {
                        if (b.getTimeout() > 0) {
                            IRCCloudLog.Log(Log.DEBUG, TAG, "Requesting backlog for timed-out buffer: bid" + b.getBid());
                            request_backlog(b.getCid(), b.getBid(), 0);
                        }

                        if(oobTasks.size() > 10)
                            break;
                    }
                    NotificationsList.getInstance().deleteOldNotifications();
                    NotificationsList.getInstance().pruneNotificationChannels();
                    if (bid != -1) {
                        Buffer b = mBuffers.getBuffer(bid);
                        if(b != null) {
                            b.setTimeout(0);
                            b.setDeferred(0);
                        }
                    }
                    oobTasks.remove(bid);
                    if(oobTasks.size() > 0)
                        oobTasks.values().toArray(new OOBFetcher[oobTasks.values().size()])[0].connect();
                    break;
                case EVENT_OOB_FAILED:
                    bid = ((OOBFetcher)object).getBid();
                    if (bid == -1) {
                        IRCCloudLog.Log(Log.ERROR, TAG, "Failed to fetch the initial backlog, reconnecting!");
                        /*try {
                            IRCCloudDatabase.getInstance().endTransaction();
                        } catch (IllegalStateException e) {

                        }*/
                        streamId = null;
                        highest_eid = 0;
                        if (client != null)
                            client.disconnect();
                        return;
                    } else {
                        Buffer b = mBuffers.getBuffer(bid);
                        if (b != null && b.getTimeout() == 1) {
                            //TODO: move this
                            int retryDelay = 1000;
                            IRCCloudLog.Log(Log.WARN, TAG, "Failed to fetch backlog for timed-out buffer, retrying in " + retryDelay + "ms");
                            idleTimer.schedule(new TimerTask() {
                                public void run() {
                                    ((OOBFetcher)object).connect();
                                }
                            }, retryDelay);
                            retryDelay *= 2;
                        } else {
                            IRCCloudLog.Log(Log.ERROR, TAG, "Failed to fetch backlog");
                            synchronized (oobTasks) {
                                oobTasks.remove(bid);
                                if(oobTasks.size() > 0)
                                    oobTasks.values().toArray(new OOBFetcher[oobTasks.values().size()])[0].connect();
                            }
                        }
                    }
                    break;
                default:
                    break;
            }

            if (message == EVENT_PROGRESS || accrued == 0) {
                for (int i = 0; i < handlers.size(); i++) {
                    IRCEventHandler handler = handlers.get(i);
                    if (handler != exclude) {
                        handler.onIRCEvent(message, object);
                    }
                }
            }
        }
    }

    public UserInfo getUserInfo() {
        return userInfo;
    }

    public class UserInfo {
        public int id;
        public String name;
        public String email;
        public boolean verified;
        public int last_selected_bid;
        public long connections;
        public long active_connections;
        public long join_date;
        public boolean auto_away;
        public String limits_name;
        public ObjectNode limits;
        public int num_invites;
        public JSONObject prefs;
        public String highlights;
        public boolean uploads_disabled;
        public String avatar;

        public UserInfo() {
        }

        public UserInfo(IRCCloudJSONObject object) {
            SharedPreferences.Editor editor = IRCCloudApplication.getInstance().getApplicationContext().getSharedPreferences("prefs", 0).edit();
            editor.putString("userinfo", object.toString());
            editor.apply();

            id = object.getInt("id");
            IRCCloudLog.Log(Log.INFO, "IRCCloud", "Setting UserInfo for uid" + id);

            name = object.getString("name");
            email = object.getString("email");
            verified = object.getBoolean("verified");
            last_selected_bid = object.getInt("last_selected_bid");
            connections = object.getLong("num_connections");
            active_connections = object.getLong("num_active_connections");
            join_date = object.getLong("join_date");
            auto_away = object.getBoolean("autoaway");
            uploads_disabled = object.has("uploads_disabled") && object.getBoolean("uploads_disabled");
            if(object.has("avatar") && object.getString("avatar").length() > 0 && !object.getString("avatar").equals("null"))
                avatar = object.getString("avatar");
            else
                avatar = null;

            if (object.has("prefs") && object.getString("prefs").length() > 0 && !object.getString("prefs").equals("null")) {
                try {
                    IRCCloudLog.Log(Log.INFO, "IRCCloud", "Prefs: " + object.getString("prefs"));
                    prefs = new JSONObject(object.getString("prefs"));
                } catch (JSONException e) {
                    IRCCloudLog.Log(Log.ERROR, "IRCCloud", "Unable to parse prefs: " + object.getString("prefs"));
                    IRCCloudLog.LogException(e);
                    prefs = null;
                }
            } else {
                IRCCloudLog.Log(Log.INFO, "IRCCloud", "User prefs not set");
                prefs = null;
            }

            limits_name = object.getString("limits_name");
            limits = object.getJsonObject("limits");

            if (object.has("highlights")) {
                JsonNode h = object.getJsonNode("highlights");
                highlights = "";
                for (int i = 0; i < h.size(); i++) {
                    if (highlights.length() > 0)
                        highlights += ", ";
                    highlights += h.get(i).asText();
                }
            }
        }
    }

    public static void printStackTraceToCrashlytics(Exception e) {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        String stack = sw.toString();
        for(String s : stack.split("\n")) {
            IRCCloudLog.Log(Log.WARN, TAG, s);
        }
    }

    public static void sendFeedbackReport(Activity ctx) {
        try {
            String bugReport = "Briefly describe the issue below:\n\n\n\n\n" +
                    "===========\n" +
                    "UID: " + PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getString("uid", "") + "\n" +
                    "App version: " + ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0).versionName + " (" + ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0).versionCode + ")\n" +
                    "Device: " + Build.MODEL + "\n" +
                    "Android version: " + Build.VERSION.RELEASE + "\n" +
                    "Firmware fingerprint: " + Build.FINGERPRINT + "\n";

            File f = new File(ctx.getFilesDir(), "logs");
            f.mkdirs();
            File output = new File(f, "log.txt");

            FileOutputStream out = new FileOutputStream(output);
            out.write(IRCCloudLog.lines().getBytes());
            out.close();

            Intent email = new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:team@irccloud.com"));
            email.putExtra(Intent.EXTRA_SUBJECT, "IRCCloud for Android");

            List<ResolveInfo> resolveInfos = ctx.getPackageManager().queryIntentActivities(email, 0);
            List<LabeledIntent> intents = new ArrayList<>();
            for (ResolveInfo info : resolveInfos) {
                Intent intent = new Intent(Intent.ACTION_SEND);
                intent.setComponent(new ComponentName(info.activityInfo.packageName, info.activityInfo.name));
                intent.setType("message/rfc822");
                intent.putExtra(Intent.EXTRA_EMAIL, new String[]{"IRCCloud Team <team@irccloud.com>"});
                intent.putExtra(Intent.EXTRA_TEXT, bugReport);
                intent.putExtra(Intent.EXTRA_SUBJECT, "IRCCloud for Android");
                intent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(ctx, ctx.getPackageName() + ".fileprovider", output));
                intents.add(new LabeledIntent(intent, info.activityInfo.packageName, info.loadLabel(ctx.getPackageManager()), info.icon));
            }
            Intent chooser = Intent.createChooser(intents.remove(intents.size() - 1), "Send Feedback:");
            chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(new LabeledIntent[intents.size()]));
            ctx.startActivity(chooser);
        } catch (Exception e) {
            Toast.makeText(ctx, "Unable to generate email report: " + e.getMessage(), Toast.LENGTH_SHORT).show();
            IRCCloudLog.LogException(e);
            NetworkConnection.printStackTraceToCrashlytics(e);
        }
    }
} public class AvatarsList {
    @Dao
    public interface AvatarsDao {
        @Query("SELECT * FROM Avatar WHERE cid = :cid AND nick = :nick LIMIT 1")
        Avatar getAvatar(int cid, String nick);

        @Query("SELECT avatar_url FROM Avatar WHERE cid = :cid AND nick = :nick LIMIT 1")
        String getAvatarURL(int cid, String nick);

        @Insert(onConflict = OnConflictStrategy.REPLACE)
        void insert(Avatar avatar);

        @Update
        void update(Avatar avatar);

        @Delete
        void delete(Avatar avatar);

        @Query("DELETE FROM Avatar")
        void clear();
    }

    private static final int MAX_AVATARS = 50;
    private HashMap<Integer, HashMap<String, Avatar>> avatars;

    private static AvatarsList instance = null;

    public synchronized static AvatarsList getInstance() {
        if (instance == null)
            instance = new AvatarsList();
        return instance;
    }

    public AvatarsList() {
        avatars = new HashMap<>();
    }

    public synchronized void clear() {
        avatars.clear();
        IRCCloudDatabase.getInstance().AvatarsDao().clear();
    }

    public static void setAvatarURL(int cid, String nick, long eid, String avatar_url) {
        if(nick != null) {
            Avatar a = IRCCloudDatabase.getInstance().AvatarsDao().getAvatar(cid, nick);

            if(avatar_url != null) {
                if (a == null) {
                    a = new Avatar();
                    a.cid = cid;
                    a.nick = nick;
                    a.eid = eid;
                    a.avatar_url = avatar_url;
                    IRCCloudDatabase.getInstance().AvatarsDao().insert(a);
                } else if (a.eid < eid && !a.avatar_url.equals(avatar_url)) {
                    a.avatar_url = avatar_url;
                    a.eid = eid;
                    IRCCloudDatabase.getInstance().AvatarsDao().update(a);
                }
            } else {
                if(a != null)
                    IRCCloudDatabase.getInstance().AvatarsDao().delete(a);
            }
        }
    }

    public static String getAvatarURL(int cid, String nick) {
        return IRCCloudDatabase.getInstance().AvatarsDao().getAvatarURL(cid, nick);
    }

    public synchronized Avatar getAvatar(int cid, String nick, String display_name) {
        Avatar a = null;

        if(avatars.containsKey(cid) && avatars.get(cid) != null) {
            if(avatars.get(cid).containsKey(nick))
                a = avatars.get(cid).get(nick);
        } else {
            avatars.put(cid, new HashMap<String, Avatar>());
        }

        if(avatars.get(cid).size() > MAX_AVATARS)
            pruneAvatars();

        if(a == null) {
            a = new Avatar();
            a.cid = cid;
            a.nick = nick;
            a.display_name = (display_name != null && display_name.length() > 0) ? display_name : nick;
            avatars.get(cid).put(nick, a);
        }

        return a;
    }

    public synchronized void pruneAvatars() {
        android.util.Log.i("IRCCloud", "Pruning avatar cache");
        for (HashMap<String, Avatar> cid : avatars.values()) {
            ArrayList<Avatar> avs = new ArrayList<>(cid.values());
            try {
                Collections.sort(avs, new Comparator<Avatar>() {
                    @Override
                    public int compare(Avatar a1, Avatar a2) {
                        return Long.compare(a2.lastAccessTime, a1.lastAccessTime);
                    }
                });
            } catch (Exception e) {
                avatars.clear();
                return;
            }
            while(avs.size() > MAX_AVATARS) {
                Avatar a = avs.get(0);
                avs.remove(a);
                cid.remove(a);
            }
        }
    }

    public static int SHORTCUT_ICON_SIZE() {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 108, IRCCloudApplication.getInstance().getApplicationContext().getResources().getDisplayMetrics());
    }

    public static IconCompat getIconForBuffer(Buffer b, ImageList.OnImageFetchedListener onImageFetchedListener) {
        IconCompat avatar = null;

        if(b.getType().equals("channel")) {
            String name = b.normalizedName();
            if(name.length() > 0) {
                name = "#" + name.substring(0,1);
            } else if(b.getName().length() > 1){
                name = b.getName().toLowerCase().substring(0,2);
            } else {
                name = "#";
            }
            avatar = IconCompat.createWithAdaptiveBitmap(Avatar.generateBitmap(name.toUpperCase(), 0xFFFFFFFF, Color.parseColor("#" + ColorScheme.colorForNick(b.getName(), false)), false, AvatarsList.SHORTCUT_ICON_SIZE(), false));
        } else if(b.isConversation()) {
            String avatar_url = getAvatarURL(b.getCid(), b.getName());
            try {
                if (avatar_url != null && avatar_url.length() > 0 && PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("avatar-images", false)) {
                    Bitmap bitmap = ImageList.getInstance().getImage(new URL(avatar_url));
                    if (bitmap != null)
                        avatar = IconCompat.createWithAdaptiveBitmap(bitmap);
                    else if(onImageFetchedListener != null)
                        ImageList.getInstance().fetchImage(new URL(avatar_url), onImageFetchedListener);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

            if(avatar == null) {
                avatar = IconCompat.createWithAdaptiveBitmap(AvatarsList.getInstance().getAvatar(b.getCid(), b.getName(), null).getBitmap(false, AvatarsList.SHORTCUT_ICON_SIZE(), false, false));
            }
        }
        return avatar;
    }

} public class BuffersList {
    public class comparator implements Comparator<Buffer> {
        public int compare(Buffer b1, Buffer b2) {
            if (b1.getCid() < b2.getCid())
                return -1;
            if (b1.getCid() > b2.getCid())
                return 1;
            if (b1.isConsole())
                return -1;
            if (b2.isConsole())
                return 1;
            if (b1.getBid() == b2.getBid())
                return 0;
            int joined1 = 1, joined2 = 1;
            Channel c = ChannelsList.getInstance().getChannelForBuffer(b1.getBid());
            if (c == null)
                joined1 = 0;
            c = ChannelsList.getInstance().getChannelForBuffer(b2.getBid());
            if (c == null)
                joined2 = 0;
            if ((b1.isConversation() || b1.isMPDM()) && (b2.isChannel() && !b2.isMPDM())) {
                return 1;
            } else if ((b1.isChannel() && !b1.isMPDM()) && (b2.isConversation() || b2.isMPDM())) {
                return -1;
            } else if (joined1 != joined2) {
                return joined2 - joined1;
            } else {
                if (comparator.compare(b1.normalizedName(), b2.normalizedName()) == 0)
                    return (b1.getBid() < b2.getBid()) ? -1 : 1;
                return comparator.compare(b1.normalizedName(), b2.normalizedName());
            }
        }
    }

    private ArrayList<Buffer> buffers;
    private SparseArray<Buffer> buffers_indexed;
    private AlphanumComparator comparator;

    private static BuffersList instance = null;
    public boolean dirty = true;

    public synchronized static BuffersList getInstance() {
        if (instance == null)
            instance = new BuffersList();
        return instance;
    }

    public BuffersList() {
        buffers = new ArrayList<>();
        buffers_indexed = new SparseArray<>();
        Collator collator = Collator.getInstance();
        collator.setStrength(Collator.SECONDARY);
        comparator = new AlphanumComparator(collator);
    }

    public void clear() {
        buffers.clear();
        buffers_indexed.clear();
        //Delete.table(Buffer.class);
    }

    public void load() {
        /*Cursor c = new Select().all().from(Buffer.class).query();
        try {
            long start = System.currentTimeMillis();
            ModelAdapter<Buffer> modelAdapter = FlowManager.getModelAdapter(Buffer.class);
            if (c != null && c.moveToFirst()) {
                buffers = new ArrayList<>(c.getCount());
                buffers_indexed = new SparseArray<>(c.getCount());
                do {
                    Buffer b = modelAdapter.loadFromCursor(c);
                    buffers.add(b);
                    buffers_indexed.put(b.getBid(), b);
                } while(c.moveToNext());
                long time = System.currentTimeMillis() - start;
                android.util.Log.i("IRCCloud", "Loaded " + c.getCount() + " buffers in " + time + "ms");
            }
        } catch (SQLiteException e) {
            buffers.clear();
            buffers_indexed.clear();
        } finally {
            if(c != null)
                c.close();
        }*/
    }

    public void save() {
        //TransactionManager.getInstance().saveOnSaveQueue(buffers);
    }


    public int count() {
        return buffers.size();
    }

    public int firstBid() {
        if (buffers_indexed.size() > 0)
            return buffers_indexed.valueAt(0).getBid();
        else
            return -1;
    }

    public synchronized Buffer createBuffer(int bid, int cid, long min_eid, long last_seen_eid, String name, String type, int archived, int deferred, int timeout, long created) {
        Buffer b = getBuffer(bid);
        if (b == null) {
            b = new Buffer();
            buffers.add(b);
            buffers_indexed.put(bid, b);
        }
        b.setBid(bid);
        b.setCid(cid);
        b.setMin_eid(min_eid);
        b.setLast_seen_eid(last_seen_eid);
        b.setName(name);
        b.setType(type);
        b.setArchived(archived);
        b.setDeferred(deferred);
        b.setTimeout(timeout);
        b.setValid(1);
        b.setCreated(created);
        if(b.getServer() != null)
            b.setServerIsSlack(b.getServer().isSlack());
        if(EventsList.getInstance().lastEidForBuffer(bid) <= last_seen_eid) {
            b.setUnread(0);
            b.setHighlights(0);
        }
        dirty = true;
        return b;
    }

    public synchronized void deleteAllDataForBuffer(int bid) {
        Buffer b = getBuffer(bid);
        if (b != null) {
            if (b.isChannel()) {
                ChannelsList.getInstance().deleteChannel(bid);
                UsersList.getInstance().deleteUsersForBuffer(b.getBid());
            }
            EventsList.getInstance().deleteEventsForBuffer(bid);
            RecentConversationsList.getInstance().deleteConversation(b.getCid(), b.getBid());
            buffers.remove(b);
            //b.delete();
        }
        buffers_indexed.remove(bid);
        dirty = true;
    }

    public synchronized Buffer getBuffer(int bid) {
        return buffers_indexed.get(bid);
    }

    public synchronized Buffer getBufferByName(int cid, String name) {
        for (Buffer b : buffers) {
            if (b.getCid() == cid && b.getName().equalsIgnoreCase(name))
                return b;
        }
        return null;
    }

    public synchronized ArrayList<Buffer> getBuffersForServer(int cid) {
        ArrayList<Buffer> list = new ArrayList<>();
        if (dirty) {
            try {
                Collections.sort(buffers, new comparator());
                dirty = false;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        for (Buffer b : buffers) {
            if (b.getCid() == cid)
                list.add(b);
        }
        return list;
    }

    public synchronized ArrayList<Buffer> getBuffers() {
        ArrayList<Buffer> list = new ArrayList<Buffer>();
        for (Buffer b : buffers) {
            list.add(b);
        }
        return list;
    }

    public synchronized void invalidate() {
        for (Buffer b : buffers) {
            b.setValid(0);
        }
    }

    public synchronized void purgeInvalidBIDs() {
        ArrayList<Buffer> buffersToRemove = new ArrayList<Buffer>();
        Iterator<Buffer> i = buffers.iterator();
        while (i.hasNext()) {
            Buffer b = i.next();
            if (b.getValid() == 0)
                buffersToRemove.add(b);
        }
        i = buffersToRemove.iterator();
        while (i.hasNext()) {
            Buffer b = i.next();
            EventsList.getInstance().deleteEventsForBuffer(b.getBid());
            ChannelsList.getInstance().deleteChannel(b.getBid());
            UsersList.getInstance().deleteUsersForBuffer(b.getBid());
            buffers.remove(b);
            buffers_indexed.remove(b.getBid());
            if (b.isConsole()) {
                ServersList.getInstance().deleteServer(b.getCid());
            }
            //b.delete();
        }
        dirty = true;
    }
} public class ChannelsList {
    private SparseArray<Channel> channels;

    private static ChannelsList instance = null;

    public synchronized static ChannelsList getInstance() {
        if (instance == null)
            instance = new ChannelsList();
        return instance;
    }

    public ChannelsList() {
        channels = new SparseArray<>();
    }

    public synchronized void clear() {
        channels.clear();
        //Delete.table(Channel.class);
    }

    public void load() {
        /*Cursor c = new Select().all().from(Channel.class).query();
        try {
            long start = System.currentTimeMillis();
            ModelAdapter<Channel> modelAdapter = FlowManager.getModelAdapter(Channel.class);
            if (c != null && c.moveToFirst()) {
                channels = new SparseArray<>(c.getCount());
                do {
                    Channel s = modelAdapter.loadFromCursor(c);
                    channels.put(s.bid, s);
                    updateMode(s.bid, s.mode, s.ops, true);
                } while(c.moveToNext());
                long time = System.currentTimeMillis() - start;
                android.util.Log.i("IRCCloud", "Loaded " + c.getCount() + " channels in " + time + "ms");
            }
        } catch (SQLiteException e) {
            channels.clear();
        } finally {
            if(c != null)
                c.close();
        }*/
    }

    public void save() {
        /*ArrayList<Channel> s = new ArrayList<>(channels.size());
        for (int i = 0; i < channels.size(); i++) {
            s.add(channels.valueAt(i));
        }
        TransactionManager.getInstance().saveOnSaveQueue(s);*/
    }

    public synchronized Channel createChannel(int cid, int bid, String name, String topic_text, long topic_time, String topic_author, String type, long timestamp) {
        Channel c = getChannelForBuffer(bid);
        if (c == null) {
            c = new Channel();
            channels.put(bid, c);
        }
        c.cid = cid;
        c.bid = bid;
        c.name = name;
        c.topic_author = topic_author;
        c.topic_text = topic_text;
        c.topic_time = topic_time;
        c.type = type;
        c.timestamp = timestamp;
        c.key = false;
        c.mode = "";
        c.valid = 1;
        return c;
    }

    public synchronized void deleteChannel(int bid) {
        channels.remove(bid);
    }

    public synchronized void updateTopic(int bid, String topic_text, long topic_time, String topic_author) {
        Channel c = getChannelForBuffer(bid);
        if (c != null) {
            c.topic_text = ColorFormatter.emojify(topic_text);
            c.topic_time = topic_time;
            c.topic_author = topic_author;
        }
    }

    public synchronized void updateMode(int bid, String mode, JsonNode ops, boolean init) {
        Channel c = getChannelForBuffer(bid);
        if (c != null && ops != null) {
            c.key = false;
            Iterator<JsonNode> iterator = ops.get("add").elements();
            while(iterator.hasNext()) {
                JsonNode m = iterator.next();
                c.addMode(m.get("mode").asText(), m.get("param").asText(), init);
            }
            iterator = ops.get("remove").elements();
            while(iterator.hasNext()) {
                JsonNode m = iterator.next();
                c.removeMode(m.get("mode").asText());
            }
            c.mode = mode;
        }
    }

    public synchronized void updateURL(int bid, String url) {
        Channel c = getChannelForBuffer(bid);
        if (c != null) {
            c.url = url;
        }
    }

    public synchronized void updateTimestamp(int bid, long timestamp) {
        Channel c = getChannelForBuffer(bid);
        if (c != null) {
            c.timestamp = timestamp;
        }
    }

    public synchronized Channel getChannelForBuffer(int bid) {
        return channels.get(bid);
    }

    public synchronized ArrayList<Channel> getChannels() {
        ArrayList<Channel> list = new ArrayList<>();
        for (int i = 0; i < channels.size(); i++) {
            Channel c = channels.valueAt(i);
            list.add(c);
        }
        return list;
    }

    public synchronized void invalidate() {
        for (int i = 0; i < channels.size(); i++) {
            Channel c = channels.valueAt(i);
            c.valid = 0;
        }
    }

    public synchronized void purgeInvalidChannels() {
        ArrayList<Channel> channelsToRemove = new ArrayList<>();
        for (int i = 0; i < channels.size(); i++) {
            Channel c = channels.valueAt(i);
            if (c.valid == 0)
                channelsToRemove.add(c);
        }
        for (Channel c : channelsToRemove) {
            android.util.Log.e("IRCCloud", "Removing invalid channel: " + c.name);
            UsersList.getInstance().deleteUsersForBuffer(c.bid);
            channels.remove(c.bid);
        }
    }
} @SuppressLint("UseSparseArrays")
public class EventsList {
    private final HashMap<Integer, TreeMap<Long, Event>> events;
    private final HashMap<Integer, TreeMap<String, Event>> msgids;
    private HashSet<Integer> loaded_bids = new HashSet<>();
    private static EventsList instance = null;
    private ColorScheme colorScheme = ColorScheme.getInstance();

    public synchronized static EventsList getInstance() {
        if (instance == null)
            instance = new EventsList();
        return instance;
    }

    public EventsList() {
        events = new HashMap<>(100);
        msgids = new HashMap<>(100);
    }

    public void load(int bid) {
        /*synchronized (events) {
            Cursor c = null;
            try {
                if(loaded_bids.contains(bid))
                    return;
                long start = System.currentTimeMillis();
                ModelAdapter<Event> modelAdapter = FlowManager.getModelAdapter(Event.class);
                if (events.containsKey(bid) && events.get(bid) != null && events.get(bid).size() > 0) {
                    c = new Select().from(Event.class).where(Condition.column(Event$Table.BID).is(bid)).and(Condition.column(Event$Table.EID).lessThan(events.get(bid).firstKey())).query();
                } else {
                    c = new Select().from(Event.class).where(Condition.column(Event$Table.BID).is(bid)).query();
                }

                if (c != null && c.moveToFirst()) {
                    android.util.Log.d("IRCCloud", "Loading events for bid" + bid);
                    do {
                        addEvent(modelAdapter.loadFromCursor(c));
                    } while (c.moveToNext());
                    long time = System.currentTimeMillis() - start;
                    android.util.Log.i("IRCCloud", "Loaded " + c.getCount() + " events in " + time + "ms");
                    loaded_bids.add(bid);
                }
            } catch (SQLiteException e) {
                NetworkConnection.printStackTraceToCrashlytics(e);
            } finally {
                if(c != null)
                    c.close();
            }
        }*/
    }

    public void save() {
        /*synchronized (events) {
            for (int bid : events.keySet()) {
                TreeMap<Long, Event> e = events.get(bid);
                if (e != null) {
                    TransactionManager.getInstance().saveOnSaveQueue(e.values());
                }
            }
        }*/
    }

    public void clear() {
        synchronized (events) {
            events.clear();
            loaded_bids.clear();
            msgids.clear();
            //Delete.table(Event.class);
        }
    }

    public void addEvent(Event event) {
        synchronized (events) {
            if (!events.containsKey(event.bid) || events.get(event.bid) == null)
                events.put(event.bid, new TreeMap<Long, Event>());

            if (event.msgid != null && event.msgid.length() > 0) {
                if (!msgids.containsKey(event.bid) || msgids.get(event.bid) == null)
                    msgids.put(event.bid, new TreeMap<String, Event>());

                msgids.get(event.bid).put(event.msgid, event);
            }

            events.get(event.bid).put(event.eid, event);
        }
    }

    public interface Formatter {
        void format(IRCCloudJSONObject event, Event e, StringBuilder sb);
    }

    private HashMap<String, Formatter> formatterMap = new HashMap<String, Formatter>() {{
        put("socket_closed", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                e.from = "";
                e.row_type = MessageViewFragment.ROW_SOCKETCLOSED;
                e.color = colorScheme.timestampColor;
                e.linkify = false;
                if(event != null) {
                    if (event.has("pool_lost"))
                        e.msg = "Connection pool lost";
                    else if (event.has("server_ping_timeout"))
                        e.msg = "Server PING timed out";
                    else if (event.has("reason") && event.getString("reason").length() > 0)
                        e.msg = sb.append("Connection lost: ").append(reason(event.getString("reason"))).toString();
                    else if (event.has("abnormal"))
                        e.msg = "Connection closed unexpectedly";
                    else
                        e.msg = "";
                }
            }
        });

        put("user_channel_mode", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.target_mode = event.getString("newmode");
                    e.chan = event.getString("channel");
                }
            }
        });

        put("buffer_msg", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.target_mode = event.getString("statusmsg");
                }
            }
        });

        put("buffer_me_msg", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from_nick = event.getString("from");
                    if(event.has("display_name") && event.getString("display_name") != null && event.getString("display_name").length() > 0)
                        e.nick = event.getString("display_name");
                    else
                        e.nick = e.from_nick;
                    e.from = "";
                }
            }
        });

        put("nickname_in_use", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = event.getString("nick");
                }
                e.msg = "is already in use";
                e.color = colorScheme.networkErrorColor;
                e.bg_color = colorScheme.errorBackgroundColor;
            }
        });

        put("unhandled_line", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = "";
                    if (event.has("command"))
                        sb.append(event.getString("command")).append(" ");
                    if (event.has("raw"))
                        sb.append(event.getString("raw"));
                    else
                        sb.append(event.getString("msg"));
                    e.msg = sb.toString();
                }
                e.color = colorScheme.networkErrorColor;
                e.bg_color = colorScheme.errorBackgroundColor;
            }
        });
        put("unparsed_line", get("unhandled_line"));

        put("connecting_cancelled", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                e.from = "";
                if(event != null) {
                    if(event.has("sts"))
                        e.msg = "Upgrading connection security";
                    else
                        e.msg = "Cancelled";
                }
                e.color = colorScheme.networkErrorColor;
                e.bg_color = colorScheme.errorBackgroundColor;
            }
        });

        put("connecting_failed", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                e.row_type = MessageViewFragment.ROW_SOCKETCLOSED;
                e.color = colorScheme.timestampColor;
                e.from = "";
                e.linkify = false;
                if(event != null) {
                    String reason = reason(event.getString("reason"));
                    if (reason != null) {
                        if(reason.equals("ssl_verify_error")) {
                            String error = SSLreason(event.getJsonNode("ssl_verify_error"));
                            if(error != null && error.length() > 0)
                                e.msg = sb.append("Strict transport security error: ").append(error).toString();
                            else
                                e.msg = "Strict transport security error";
                        } else {
                            e.msg = sb.append("Failed to connect: ").append(reason).toString();
                        }
                    } else {
                        e.msg = "Failed to connect.";
                    }
                }
            }
        });

        put("quit_server", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                e.from = "";
                e.msg = "⇐ You disconnected";
                e.color = colorScheme.timestampColor;
                e.self = false;
            }
        });

        put("self_details", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                e.bg_color = colorScheme.statusBackgroundColor;
                e.linkify = false;
                if(event != null) {
                    e.from = "";
                    if(event.has("usermask") && event.has("user") && event.getString("user").length() > 0) {
                        e.msg = sb.append("<pre>Your hostmask: <b>").append(event.getString("usermask")).append("</b></pre>").toString();
                        sb.setLength(0);
                        if(event.has("server_realname")) {
                            Event e1 = new Event(e);
                            e1.eid++;
                            e1.msg = sb.append("<pre>Your name: <b>").append(event.getString("server_realname")).append("</b></pre>").toString();
                            e1.linkify = true;
                            addEvent(e1);
                        }
                    } else if(event.has("server_realname")) {
                        e.msg = sb.append("<pre>Your name: <b>").append(event.getString("server_realname")).append("</b></pre>").toString();
                        e.linkify = true;
                    }
                }
            }
        });

        put("myinfo", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = "";
                    sb.append("Host: ").append(event.getString("server")).append("\n");
                    sb.append("IRCd: ").append(event.getString("version")).append("\n");
                    sb.append("User modes: ").append(event.getString("user_modes")).append("\n");
                    sb.append("Channel modes: ").append(event.getString("channel_modes")).append("\n");
                    if (event.has("rest") && event.getString("rest").length() > 0)
                        sb.append("Parametric channel modes: ").append(event.getString("rest")).append("\n");
                    e.msg = "<pre>" + TextUtils.htmlEncode(sb.toString()) + "</pre>";
                }
                e.bg_color = colorScheme.statusBackgroundColor;
                e.linkify = false;
            }
        });

        put("user_mode", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = "";
                    e.msg = sb.append("<pre>Your user mode is: <b>+").append(event.getString("newmode")).append("</b></pre>").toString();
                }
                e.bg_color = colorScheme.statusBackgroundColor;
            }
        });

        put("your_unique_id", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = "";
                    e.msg = sb.append("<pre>Your unique ID is: <b>").append(event.getString("unique_id")).append("</b></pre>").toString();
                }
                e.bg_color = colorScheme.statusBackgroundColor;
            }
        });

        put("kill", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = "";
                    sb.append("You were killed");
                    if (event.has("from"))
                        sb.append(" by ").append(event.getString("from"));
                    if (event.has("killer_hostmask"))
                        sb.append(" (").append(event.getString("killer_hostmask")).append(")");
                    if (event.has("reason"))
                        sb.append(": ").append(TextUtils.htmlEncode(event.getString("reason")));
                    e.msg = sb.toString();
                }
                e.bg_color = colorScheme.statusBackgroundColor;
                e.linkify = false;
            }
        });

        put("banned", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = "";
                    sb.append("You were banned");
                    if (event.has("server"))
                        sb.append(" from ").append(event.getString("server"));
                    if (event.has("reason"))
                        sb.append(": ").append(TextUtils.htmlEncode(event.getString("reason")));
                    e.msg = sb.toString();
                }
                e.bg_color = colorScheme.statusBackgroundColor;
                e.linkify = false;
            }
        });

        put("channel_topic", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    if (event.has("author"))
                        e.from = event.getString("author");
                    else
                        e.from = event.getString("server");
                    if (event.getString("topic") != null && event.getString("topic").length() > 0)
                        e.msg = sb.append("set the topic: ").append(TextUtils.htmlEncode(event.getString("topic"))).toString();
                    else
                        e.msg = "cleared the topic";
                }
                e.bg_color = colorScheme.statusBackgroundColor;
            }
        });

        put("channel_mode", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.nick = e.from;
                    e.from = "";
                    e.msg = sb.append("Channel mode set to: <b>").append(event.getString("diff")).append("</b>").toString();
                }
                e.bg_color = colorScheme.statusBackgroundColor;
                e.linkify = false;
                e.self = false;
            }
        });

        put("channel_mode_is", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = "";
                    if (event.getString("diff") != null && event.getString("diff").length() > 0)
                        e.msg = sb.append("Channel mode is: <b>").append(event.getString("diff")).append("</b>").toString();
                    else
                        e.msg = "No channel mode";
                }
                e.bg_color = colorScheme.statusBackgroundColor;
                e.linkify = false;
            }
        });

        put("kicked_channel", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = "";
                    e.from_mode = event.getString("kicker_mode");
                    e.from_hostmask = event.getString("kicker_hostmask");
                    e.old_nick = event.getString("nick");
                    e.nick = event.getString("kicker");
                }
                e.color = colorScheme.timestampColor;
                e.linkify = false;
                if (e.self)
                    e.row_type = MessageViewFragment.ROW_SOCKETCLOSED;
            }
        });
        put("you_kicked_channel", get("kicked_channel"));

        put("channel_mode_list_change", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    boolean unknown = true;
                    JsonNode ops = event.getJsonObject("ops");
                    if (ops != null) {
                        JsonNode add = ops.get("add");
                        if (add != null && add.size() > 0) {
                            JsonNode op = add.get(0);
                            if (op.get("mode").asText().equals("b")) {
                                e.nick = e.from;
                                e.from = "";
                                e.msg = sb.append("banned <b>").append(op.get("param").asText()).append("</b> (<font color=#808080>+b</font>)").toString();
                                unknown = false;
                            } else if (op.get("mode").asText().equals("e")) {
                                e.nick = e.from;
                                e.from = "";
                                e.msg = sb.append("exempted <b>").append(op.get("param").asText()).append("</b> from bans (<font color=#808080>+e</font>)").toString();
                                unknown = false;
                            } else if (op.get("mode").asText().equals("q")) {
                                if (op.get("param").asText().contains("@") || op.get("param").asText().contains("$")) {
                                    e.nick = e.from;
                                    e.from = "";
                                    e.msg = sb.append("quieted <b>").append(op.get("param").asText()).append("</b> (<font color=#808080>+q</font>)").toString();
                                } else {
                                    e.type = "user_channel_mode";
                                    e.chan = event.getString("channel");
                                    e.nick = op.get("param").asText();
                                }
                                unknown = false;
                            } else if (op.get("mode").asText().equals("I")) {
                                e.nick = e.from;
                                e.from = "";
                                e.msg = sb.append("added <b>").append(op.get("param").asText()).append("</b> to the invite list (<font color=#808080>+I</font>)").toString();
                                unknown = false;
                            }
                        }
                        JsonNode remove = ops.get("remove");
                        if (remove != null && remove.size() > 0) {
                            JsonNode op = remove.get(0);
                            if (op.get("mode").asText().equals("b")) {
                                e.nick = e.from;
                                e.from = "";
                                e.msg = sb.append("un-banned <b>").append(op.get("param").asText()).append("</b> (<font color=#808080>-b</font>)").toString();
                                unknown = false;
                            } else if (op.get("mode").asText().equals("e")) {
                                e.nick = e.from;
                                e.from = "";
                                e.msg = sb.append("un-exempted <b>").append(op.get("param").asText()).append("</b> from bans (<font color=#808080>-e</font>)").toString();
                                unknown = false;
                            } else if (op.get("mode").asText().equals("q")) {
                                if (op.get("param").asText().contains("@") || op.get("param").asText().contains("$")) {
                                    e.nick = e.from;
                                    e.from = "";
                                    e.msg = sb.append("un-quieted <b>").append(op.get("param").asText()).append("</b> (<font color=#808080>-q</font>)").toString();
                                } else {
                                    e.type = "user_channel_mode";
                                    e.chan = event.getString("channel");
                                    e.nick = op.get("param").asText();
                                }
                                unknown = false;
                            } else if (op.get("mode").asText().equals("I")) {
                                e.nick = e.from;
                                e.from = "";
                                e.msg = sb.append("removed <b>").append(op.get("param").asText()).append("</b> from the invite list (<font color=#808080>-I</font>)").toString();
                                unknown = false;
                            }
                        }
                    }
                    if (unknown) {
                        e.nick = e.from;
                        e.from = "";
                        e.msg = sb.append("set channel mode: <b>").append(event.getString("diff")).append("</b>").toString();
                    }
                }
                e.bg_color = colorScheme.statusBackgroundColor;
                e.linkify = false;
                e.self = false;
            }
        });

        put("motd_response", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    JsonNode lines = event.getJsonNode("lines");
                    e.from = "";
                    if (lines != null) {
                        sb.append("<pre>");
                        if (event.has("start"))
                            sb.append(event.getString("start")).append("<br/>");
                        for (int i = 0; i < lines.size(); i++) {
                            sb.append(TextUtils.htmlEncode(lines.get(i).asText()).replace("  ", " &nbsp;")).append("<br/>");
                        }
                        sb.append("</pre>");
                        e.msg = sb.toString();
                    }
                }
                e.bg_color = colorScheme.selfBackgroundColor;
            }
        });
        put("server_motd", get("motd_response"));
        put("info_response", get("motd_response"));

        put("notice", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.chan = event.getString("target");
                    e.nick = event.getString("target");
                    e.target_mode = event.getString("statusmsg");
                    e.msg = sb.append("<pre>").append(e.msg).append("</pre>").toString();
                }
                e.bg_color = colorScheme.noticeBackgroundColor;
            }
        });

        put("buffer_msg", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.target_mode = event.getString("statusmsg");
                }
            }
        });

        put("newsflash", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.msg = sb.append("<pre>").append(e.msg).append("</pre>").toString();
                }
                e.bg_color = colorScheme.noticeBackgroundColor;
            }
        });

        put("invited", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = event.getString("inviter");
                    e.msg = sb.append("<pre>invited ").append(event.getString("invitee")).append(" to join ").append(event.getString("channel")).append("</pre>").toString();
                }
                e.bg_color = colorScheme.noticeBackgroundColor;
            }
        });

        put("generic_server_info", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.msg = sb.append("<pre>").append(e.msg).append("</pre>").toString();
                }
                e.bg_color = colorScheme.noticeBackgroundColor;
            }
        });

        put("rehashed_config", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.msg = sb.append("<pre>Rehashed config: ").append(event.getString("file")).append("(").append(e.msg).append(")</pre>").toString();
                }
                e.bg_color = colorScheme.noticeBackgroundColor;
            }
        });

        put("knock", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    if (e.nick != null && e.nick.length() > 0) {
                        sb.append("<pre>").append(e.msg);
                        e.from = e.nick;
                        if (e.hostmask != null && e.hostmask.length() > 0)
                            sb.append(" (").append(e.hostmask).append(")");
                    } else {
                        sb.append("<pre>").append(event.getString("userhost")).append(" ").append(e.msg);
                    }
                    sb.append("</pre>");
                    e.msg = sb.toString();
                }
                e.bg_color = colorScheme.noticeBackgroundColor;
            }
        });

        put("hidden_host_set", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                e.bg_color = colorScheme.statusBackgroundColor;
                e.linkify = false;
                if(event != null) {
                    e.from = "";
                    e.msg = sb.append("<b>").append(event.getString("hidden_host")).append("</b> ").append(e.msg).toString();
                }
            }
        });

        put("inviting_to_channel", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = "";
                    e.msg = sb.append("<pre>You invited ").append(event.getString("recipient")).append(" to join ").append(event.getString("channel")).append("</pre>").toString();
                }
                e.bg_color = colorScheme.noticeBackgroundColor;
            }
        });

        put("channel_invite", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.msg = sb.append("<pre>Invite to join ").append(event.getString("channel")).append("</pre>").toString();
                    e.old_nick = event.getString("channel");
                }
                e.bg_color = colorScheme.noticeBackgroundColor;
            }
        });

        put("callerid", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = e.nick;
                    e.msg = sb.append("<pre>").append(e.msg).append("</pre>").toString();
                    e.highlight = true;
                    e.linkify = false;
                    e.hostmask = event.getString("usermask");
                }
            }
        });

        put("target_callerid", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = event.getString("target_nick");
                    e.msg = sb.append("<pre>").append(e.msg).append("</pre>").toString();
                }
                e.color = colorScheme.networkErrorColor;
                e.bg_color = colorScheme.errorBackgroundColor;
            }
        });

        put("target_notified", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = event.getString("target_nick");
                    e.msg = sb.append("<pre>").append(e.msg).append("</pre>").toString();
                }
                e.color = colorScheme.networkErrorColor;
                e.bg_color = colorScheme.errorBackgroundColor;
            }
        });

        put("link_channel", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    if (event.has("invalid_chan")) {
                        if (event.has("valid_chan")) {
                            e.msg = sb.append(event.getString("invalid_chan")).append(" → ").append(event.getString("valid_chan")).append(" ").append(e.msg).toString();
                        } else {
                            e.msg = sb.append(event.getString("invalid_chan")).append(" ").append(e.msg).toString();
                        }
                    }
                }
                e.color = colorScheme.networkErrorColor;
                e.bg_color = colorScheme.errorBackgroundColor;
            }
        });

        String[] statuses = {
                "server_motdstart",
                "server_welcome",
                "server_motd",
                "server_endofmotd",
                "server_nomotd",
                "server_luserclient",
                "server_luserop",
                "server_luserconns",
                "server_luserme",
                "server_n_local",
                "server_luserchannels",
                "server_n_global",
                "server_yourhost",
                "server_created",
                "server_luserunknown",
                "server_snomask",
                "starircd_welcome",
                "zurna_motd",
                "wait",
                "logged_in_as",
                "btn_metadata_set",
                "sasl_success",
                "you_are_operator",
                "codepage",
                "logged_out",
                "nick_locked",
                "text",
                "admin_info",
                "error"
        };
        Formatter statusFormatter = new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                e.bg_color = colorScheme.statusBackgroundColor;
                if(event != null) {
                    e.msg = sb.append("<pre>").append(e.msg).append("</pre>").toString();
                    e.from = "";
                    if (!e.type.equals("server_motd") && !e.type.equals("zurna_motd"))
                        e.linkify = false;
                }
            }
        };
        for (String status : statuses)
            put(status, statusFormatter);

        String[] stats = {
                "stats", "statslinkinfo", "statscommands", "statscline", "statsnline", "statsiline", "statskline", "statsqline", "statsyline", "statsbline", "statsgline", "statstline", "statseline", "statsvline", "statslline", "statsuptime", "statsoline", "statshline", "statssline", "statsuline", "statsdebug", "spamfilter", "endofstats"
        };
        Formatter statsFormatter = new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = "";
                    sb.append("<pre>");
                    if (event.has("parts") && event.getString("parts").length() > 0)
                        sb.append(event.getString("parts")).append(": ");
                    sb.append(e.msg).append("</pre>");
                    e.msg = sb.toString();
                }
                e.bg_color = colorScheme.statusBackgroundColor;
                e.linkify = false;
            }
        };
        for (String stat : stats)
            put(stat, statsFormatter);

        String[] caps = {
                "cap_ls", "cap_req", "cap_ack", "cap_raw", "cap_new", "cap_list", "cap_invalid"
        };
        Formatter capsFormatter = new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    sb.append("<pre>");
                    e.from = "";
                    e.linkify = false;
                    switch (e.type) {
                        case "cap_ls":
                            sb.append("<b>CAP</b> Server supports: ");
                            break;
                        case "cap_req":
                            sb.append("<b>CAP</b> Requesting: ");
                            break;
                        case "cap_ack":
                            sb.append("<b>CAP</b> Acknowledged: ");
                            break;
                        case "cap_nak":
                            sb.append("<b>CAP</b> Rejected: ");
                            break;
                        case "cap_raw":
                            sb.append("<b>CAP</b> ").append(event.getString("line"));
                            break;
                        case "cap_new":
                            sb.append("<b>CAP</b> Server added: ");
                            break;
                        case "cap_del":
                            sb.append("<b>CAP</b> Server removed: ");
                            break;
                        case "cap_list":
                            sb.append("<b>CAP</b> Enabled: ");
                            break;
                        case "cap_invalid":
                            sb.append("<b>CAP</b> ").append(event.getString("msg"));
                            break;
                    }
                    JsonNode caps = event.getJsonNode("caps");
                    if (caps != null) {
                        for (int i = 0; i < caps.size(); i++) {
                            if (i > 0)
                                sb.append(" | ");
                            sb.append(caps.get(i).asText());
                        }
                    }
                    sb.append("</pre>");
                    e.msg = sb.toString();
                }
                e.bg_color = colorScheme.statusBackgroundColor;
            }
        };
        for (String cap : caps)
            put(cap, capsFormatter);

        String[] helps = {
                "help_topics_start", "help_topics", "help_topics_end", "helphdr", "helpop", "helptlr", "helphlp", "helpfwd", "helpign"
        };
        Formatter helpsFormatter = new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                e.bg_color = colorScheme.statusBackgroundColor;
                if(event != null) {
                    e.msg = sb.append("<pre>").append(e.msg).append("</pre>").toString();
                    e.from = "";
                }
            }
        };
        for (String help : helps)
            put(help, helpsFormatter);

        String[] errors = {
                "too_fast", "sasl_fail", "sasl_too_long", "sasl_aborted", "sasl_already", "no_bots", "msg_services", "bad_ping", "not_for_halfops", "ambiguous_error_message", "list_syntax", "who_syntax"
        };
        Formatter errorFormatter = new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = "";
                }
                e.color = colorScheme.networkErrorColor;
                e.bg_color = colorScheme.errorBackgroundColor;
            }
        };
        for (String error : errors)
            put(error, errorFormatter);

        put("version", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = "";
                    e.msg = sb.append("<pre><b>").append(event.getString("server_version")).append("</b> ").append(event.getString("comments")).append("</pre>").toString();
                }
                e.bg_color = colorScheme.statusBackgroundColor;
                e.linkify = false;
            }
        });

        put("services_down", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = event.getString("services_name");
                }
                e.color = colorScheme.networkErrorColor;
                e.bg_color = colorScheme.errorBackgroundColor;
            }
        });

        put("unknown_umode", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = "";
                    if (event.has("flag"))
                        e.msg = sb.append("<b>").append(event.getString("flag")).append("</b> ").append(e.msg).toString();
                }
                e.color = colorScheme.networkErrorColor;
                e.bg_color = colorScheme.errorBackgroundColor;
            }
        });

        put("kill_deny", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = event.getString("channel");
                }
                e.color = colorScheme.networkErrorColor;
                e.bg_color = colorScheme.errorBackgroundColor;
            }
        });

        put("chan_own_priv_needed", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = event.getString("channel");
                }
                e.color = colorScheme.networkErrorColor;
                e.bg_color = colorScheme.errorBackgroundColor;
            }
        });

        put("chan_forbidden", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.from = event.getString("channel");
                }
                e.color = colorScheme.networkErrorColor;
                e.bg_color = colorScheme.errorBackgroundColor;
            }
        });

        put("list_usage", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.msg = sb.append("<pre>").append(e.msg).append("</pre>").toString();
                }
                e.bg_color = colorScheme.noticeBackgroundColor;
            }
        });

        put("time", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                e.bg_color = colorScheme.statusBackgroundColor;
                if(event != null) {
                    sb.append("<pre>").append(event.getString("time_string"));
                    if (event.has("time_stamp") && event.getString("time_stamp").length() > 0)
                        sb.append(" (").append(event.getString("time_stamp")).append(")");
                    sb.append(" — <b>").append(event.getString("time_server")).append("</b></pre>");
                    e.msg = sb.toString();
                    e.linkify = false;
                }
            }
        });

        put("watch_status", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                e.bg_color = colorScheme.statusBackgroundColor;
                if(event != null) {
                    e.from = event.getString("watch_nick");
                    e.msg = sb.append("<pre>").append(e.msg).append(" (").append(event.getString("username")).append("@").append(event.getString("userhost")).append(")</pre>").toString();
                    e.linkify = false;
                }
            }
        });

        put("sqline_nick", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                e.bg_color = colorScheme.statusBackgroundColor;
                if(event != null) {
                    e.from = event.getString("charset");
                    e.msg = sb.append("<pre>").append(e.msg).append("</pre>").toString();
                }
            }
        });

        put("you_parted_channel", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                e.row_type = MessageViewFragment.ROW_SOCKETCLOSED;
            }
        });


        put("user_chghost", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.msg = sb.append("changed host: ").append(event.getString("user")).append("@").append(event.getString("userhost")).append(" → ").append(event.getString("from_name")).append("@").append(event.getString("from_host")).toString();
                }
                e.color = colorScheme.collapsedRowTextColor;
                e.linkify = false;
            }
        });

        put("loaded_module", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                e.bg_color = colorScheme.statusBackgroundColor;
                e.linkify = false;
                if(event != null) {
                    e.msg = sb.append("<pre><b>").append(event.getString("module")).append("</b> ").append(e.msg).append("</pre>").toString();
                }
            }
        });

        put("unloaded_module", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                e.bg_color = colorScheme.statusBackgroundColor;
                e.linkify = false;
                if(event != null) {
                    e.msg = sb.append("<pre><b>").append(event.getString("module")).append("</b> ").append(e.msg).append("</pre>").toString();
                }
            }
        });

        put("invite_notify", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.msg = sb.append("invited ").append(event.getString("target")).append(" to join ").append(event.getString("channel")).toString();
                }
            }
        });

        put("channel_name_change", new Formatter() {
            @Override
            public void format(IRCCloudJSONObject event, Event e, StringBuilder sb) {
                if(event != null) {
                    e.msg = sb.append("renamed the channel: ").append(event.getString("old_name")).append(" → <b>").append(event.getString("new_name")).append("</b>").toString();
                }
                e.color = colorScheme.collapsedRowTextColor;
            }
        });

    }};

    private final StringBuilder eventStringBuilder = new StringBuilder(1024);
    public Event addEvent(IRCCloudJSONObject event) {
        synchronized (events) {
            if (!events.containsKey(event.bid()))
                events.put(event.bid(), new TreeMap<Long, Event>());

            Event e = getEvent(event.eid(), event.bid());
            if (e == null) {
                e = new Event();
                events.get(event.bid()).put(event.eid(), e);
            }
            e.cid = event.cid();
            e.bid = event.bid();
            e.eid = event.eid();
            e.type = event.type();
            e.color = colorScheme.messageTextColor;
            e.bg_color = colorScheme.contentBackgroundColor;
            e.msg = event.getString("msg");
            if(e.msg != null)
                e.msg = Normalizer.normalize(e.msg, Normalizer.Form.NFC);
            e.hostmask = event.getString("hostmask");
            if(event.has("from") && event.has("display_name"))
                e.from = event.getString("display_name");
            else
                e.from = event.getString("from");
            e.from_nick = event.getString("from");
            e.from_mode = event.getString("from_mode");
            e.from_realname = event.getString("from_realname");
            if(e.from_realname != null && e.from_realname.length() > 0 && e.from_realname.equals(e.from))
                e.from_realname = null;
            if(event.has("server_time"))
                e.server_time = event.getLong("server_time");
            else
                e.server_time = 0;

            e.chan = event.getString("chan");
            if (event.has("newnick"))
                e.nick = event.getString("newnick");
            else if (event.has("nick"))
                e.nick = event.getString("nick");
            else
                e.nick = null;
            e.old_nick = event.getString("oldnick");
            e.server = event.getString("server");
            e.diff = event.getString("diff");
            e.highlight = event.getBoolean("highlight");
            e.self = event.getBoolean("self");
            e.to_chan = event.getBoolean("to_chan");
            e.to_buffer = event.getBoolean("to_buffer");
            e.ops = event.getJsonNode("ops");
            e.entities = event.getJsonNode("entities");
            e.avatar = event.getString("avatar");
            e.avatar_url = event.getString("avatar_url");
            e.msgid = event.getString("msgid");

            if((e.type.equals("buffer_msg") || e.type.equals("buffer_me_msg")) && (e.from == null || e.from.length() == 0))
                e.from = e.from_nick = e.server;

            if((e.from == null || e.from.length() == 0) && e.nick != null && e.nick.length() > 0 && event.has("display_name"))
                e.nick = event.getString("display_name");

            if (event.has("reqid"))
                e.reqid = event.getInt("reqid");
            else
                e.reqid = -1;

            if (e.from != null)
                e.from = TextUtils.htmlEncode(e.from);

            if (e.msg != null) {
                e.msg = TextUtils.htmlEncode(e.msg).replace("  ", "&nbsp; ");
                if(e.msg.startsWith(" "))
                    e.msg = "&nbsp;" + e.msg.substring(1);
            }

            synchronized (eventStringBuilder) {
                Formatter f = formatterMap.get(e.type);
                if (f != null)
                    f.format(event, e, eventStringBuilder);
                eventStringBuilder.setLength(0);
                if (event.has("value") && !event.type().startsWith("cap_")) {
                    e.msg = eventStringBuilder.append("<pre>").append(event.getString("value")).append(" ").append(e.msg).append("</pre>").toString();
                    eventStringBuilder.setLength(0);
                }
            }

            if (e.highlight)
                e.bg_color = colorScheme.highlightBackgroundColor;

            if (e.self && !e.type.equals("notice"))
                e.bg_color = colorScheme.selfBackgroundColor;

            if(e.from_nick == null && e.from != null && e.from.length() > 0)
                e.from_nick = e.from;

            if (e.msgid != null && e.msgid.length() > 0) {
                if (!msgids.containsKey(e.bid) || msgids.get(e.bid) == null)
                    msgids.put(e.bid, new TreeMap<String, Event>());

                msgids.get(e.bid).put(e.msgid, e);
            }

            return e;
        }
    }

    public int getSizeOfBuffer(int bid) {
        synchronized (events) {
            load(bid);
            TreeMap<Long, Event> buffer = events.get(bid);
            if(buffer != null)
                return buffer.size();
        }
        return 0;
    }

    public Event getEvent(long eid, int bid) {
        synchronized (events) {
            TreeMap<Long, Event> buffer = events.get(bid);
            if(buffer != null)
                return buffer.get(eid);
        }
        return null;
    }

    public void deleteEvent(long eid, int bid) {
        synchronized (events) {
            TreeMap<Long, Event> buffer = events.get(bid);
            if(buffer != null) {
                Event e = buffer.get(eid);
                if(e != null) {
                    buffer.remove(eid);
                    if (e.msgid != null && e.msgid.length() > 0 && msgids.get(bid) != null)
                        msgids.get(bid).remove(e.msgid);
                }
            }
        }
        //new Delete().from(Event.class).where(Condition.column(Event$Table.BID).is(bid)).and(Condition.column(Event$Table.EID).is(eid)).queryClose();
    }

    public void deleteEventsForBuffer(int bid) {
        synchronized (events) {
            if (events.containsKey(bid) && events.get(bid) != null)
                events.remove(bid);

            if (msgids.containsKey(bid) && msgids.get(bid) != null)
                msgids.remove(bid);
        }
        //new Delete().from(Event.class).where(Condition.column(Event$Table.BID).is(bid)).queryClose();
    }

    public synchronized void deleteEventsBeforeEid(int bid, long eid) {
        Event[] i;
        synchronized (events) {
            TreeMap<Long, Event> e = events.get(bid);
            if(e != null)
                i = e.values().toArray(new Event[0]);
            else
                return;
        }
        for (Event e : i) {
            if(e.eid <= eid) {
                deleteEvent(e.eid, e.bid);
            }
        }
    }

    public TreeMap<Long, Event> getEventsForBuffer(int bid) {
        synchronized (events) {
            load(bid);
            if (events.containsKey(bid) && events.get(bid) != null) {
                return events.get(bid);
            }
        }
        return null;
    }

    public Long lastEidForBuffer(int bid) {
        synchronized (events) {
            if (events.containsKey(bid) && events.get(bid) != null && events.get(bid).size() > 0) {
                Long[] eids = events.get(bid).keySet().toArray(new Long[events.get(bid).keySet().size()]);
                if (eids.length > 0) {
                    int i = eids.length - 1;
                    long eid = eids[i];
                    while(events.get(bid).get(eid).pending && i > 0) {
                        eid = eids[--i];
                    }
                    return eid;
                }
            /*} else {
                Event e = new Select().from(Event.class).where(Condition.column(Event$Table.BID).is(bid)).orderBy(true, Event$Table.EID).limit(1).querySingle();
                if(e != null) {
                    return e.eid;
                }*/
            }
        }
        return 0L;
    }

    public void pruneEvents(int bid) {
        synchronized (events) {
            load(bid);
            TreeMap<Long, Event> e = events.get(bid);
            while (e != null && e.size() > 50 && e.firstKey() != null) {
                Event ev = e.get(e.firstKey());
                if(ev.msgid != null && ev.msgid.length() > 0 && msgids.get(ev.bid) != null)
                    msgids.get(ev.bid).remove(ev.msgid);
                e.remove(e.firstKey());
            }
            /*if(e != null)
                new Delete().from(Event.class).where(Condition.column(Event$Table.BID).is(bid)).and(Condition.column(Event$Table.EID).lessThan(e.firstKey())).queryClose();*/
        }
    }

    public synchronized void clearCaches() {
        synchronized (events) {
            for (int bid : events.keySet()) {
                if (events.containsKey(bid) && events.get(bid) != null) {
                    for (Event e : events.get(bid).values()) {
                        e.color = colorScheme.messageTextColor;
                        e.bg_color = colorScheme.contentBackgroundColor;
                        e.timestamp = null;
                        e.html = null;
                        e.formatted = null;
                        e.formatted_nick = null;
                        e.formatted_realname = null;
                        e.linkified = false;
                        synchronized (eventStringBuilder) {
                            Formatter f = formatterMap.get(e.type);
                            if (f != null)
                                f.format(null, e, eventStringBuilder);
                            eventStringBuilder.setLength(0);
                        }
                        if (e.highlight)
                            e.bg_color = colorScheme.highlightBackgroundColor;

                        if (e.self)
                            e.bg_color = colorScheme.selfBackgroundColor;
                    }
                }
            }
        }
    }

    public synchronized Event findPendingEventForReqid(int bid, int reqid) {
        synchronized (events) {
            load(bid);
            for (Event e : events.get(bid).values()) {
                if(e.reqid == reqid && (e.pending || e.failed))
                    return e;
            }
        }
        return null;
    }


    public synchronized void clearPendingEvents(int bid) {
        Event[] i;
        synchronized (events) {
            i = events.get(bid).values().toArray(new Event[events.get(bid).values().size()]);
        }
        for (Event e : i) {
            if(e.pending || e.failed) {
                if(e.expiration_timer != null) {
                    try {
                        e.expiration_timer.cancel();
                    } catch (Exception e1) {
                        //Timer already cancelled
                    }
                    e.expiration_timer = null;
                }
                deleteEvent(e.eid, e.bid);
            }
        }
    }

    public synchronized Event getEvent(int bid, String msgid) {
        if(msgids.get(bid) != null)
            return msgids.get(bid).get(msgid);
        else
            return null;
    }

    private static final HashMap<String, String> REASONS = new HashMap<String, String>() {{
        put("pool_lost", "Connection pool failed");
        put("no_pool", "No available connection pools");
        put("enetdown", "Network down");
        put("etimedout", "Timed out");
        put("timeout", "Timed out");
        put("ehostunreach", "Host unreachable");
        put("econnrefused", "Connection refused");
        put("nxdomain", "Invalid hostname");
        put("server_ping_timeout", "PING timeout");
        put("ssl_certificate_error", "SSL certificate error");
        put("ssl_error", "SSL error");
        put("crash", "Connection crashed");
        put("networks", "You've exceeded the connection limit for free accounts.");
        put("passworded_servers", "You can't connect to passworded servers with free accounts.");
        put("unverified", "You can’t connect to external servers until you confirm your email address.");
    }};

    private static final HashMap<String, HashMap<String, String>> SSL_REASONS = new HashMap<String, HashMap<String, String>>() {{
        put("bad_cert", new HashMap<String, String>() {{
            put("unknown_ca", "Unknown certificate authority");
            put("selfsigned_peer", "Self signed certificate");
            put("cert_expired", "Certificate expired");
            put("invalid_issuer", "Invalid certificate issuer");
            put("invalid_signature", "Invalid certificate signature");
            put("name_not_permitted", "Invalid certificate alternative hostname");
            put("missing_basic_constraint", "Missing certificate basic contraints");
            put("invalid_key_usage", "Invalid certificate key usage");
        }});
        put("ssl_verify_hostname", new HashMap<String, String>() {{
            put("unable_to_match_altnames", "Certificate hostname mismatch");
            put("unable_to_match_common_name", "Certificate hostname mismatch");
            put("unable_to_decode_common_name", "Invalid certificate hostname");
        }});
    }};

    public static String reason(String reason) {
        if(reason != null && reason.length() > 0 && REASONS.containsKey(reason))
            return REASONS.get(reason);
        return reason;
    }

    public static String SSLreason(JsonNode info) {
        if(SSL_REASONS.containsKey(info.get("type").asText()) && SSL_REASONS.get(info.get("type").asText()).containsKey(info.get("error").asText())) {
            return SSL_REASONS.get(info.get("type").asText()).get(info.get("error").asText());
        }

        return info.get("type").asText() + ": " + info.get("error").asText();
    }
} public class ImageList {
    private static ImageList instance = null;
    private final HashMap<String, Bitmap> images;
    private final HashMap<String, GifDrawable> GIFs;
    private final ArrayList<String> failedURLs;
    private final ArrayList<String> activeDownloads;
    private final HashMap<String, ImageURLInfo> urlInfo;
    private final HashMap<String, ArrayList<OnImageFetchedListener>> downloadListeners;
    private final BlockingQueue<Runnable> mWorkQueue = new LinkedBlockingQueue<>();
    private static final int KEEP_ALIVE_TIME = 10;
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
    private final ThreadPoolExecutor mDownloadThreadPool = new ThreadPoolExecutor(
            2,       // Initial pool size
            4,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            mWorkQueue);

    public static boolean isImageURL(String url) {
        try {
            Uri uri = Uri.parse(url);
            if (uri != null && (uri.getScheme().startsWith("http") || uri.getScheme().startsWith(IRCCloudApplication.getInstance().getResources().getString(R.string.IMAGE_SCHEME))) && uri.getLastPathSegment() != null && uri.getLastPathSegment().contains(".")) {
                String extension = uri.getLastPathSegment().substring(uri.getLastPathSegment().indexOf(".") + 1).toLowerCase();
                if (extension.equals("jpg") || extension.equals("jpeg") || extension.equals("png") || extension.equals("gif") || extension.equals("bmp") || extension.equals("webp"))
                    return true;
            }
        } catch (Exception e) {
        }
        return url != null && url.matches(
            "(^https?://(www\\.)?flickr\\.com/photos/.*$)|" +
                //"(^https?://(www\\.)?instagram\\.com/p/.*$)|(^https?://(www\\.)?instagr\\.am/p/.*$)|" +
                    "(^https?://(www\\.)?imgur\\.com/.*$)|(^https?://m\\.imgur\\.com/.*$)|" +
                    "(^https?://d\\.pr/i/.*)|(^https?://droplr\\.com/i/.*)|" +
                    "(^https?://cl\\.ly/.*)|" +
                    "(^https?://(www\\.)?leetfiles\\.com/image/.*)|" +
                    "(^https?://(www\\.)?leetfil\\.es/image/.*)|" +
                    "(^https?://i.imgur.com/.*\\.gifv$)|" +
                    "(^https?://(www\\.)?giphy\\.com/gifs/.*)|" +
                    "(^https?://gph\\.is/.*)|" +
                    "(^https?://.*\\.twimg\\.com/media/.*\\.(png|jpe?g|gif|bmp):[a-z]+$)|" +
                    "(^https?://(www\\.)?xkcd\\.com/[0-9]+/?)|" +
                    "(^https?://.*\\.steampowered\\.com/ugc/.*)|" +
                    "(^https?://static\\.irccloud-cdn\\.com/avatar-redirect/s.*)|" +
                    "(^https?://.*\\.slack-edge\\.com/.*-(72|192|512))|" +
                    "(^https?://(www\\.)?gravatar\\.com/avatar/.*)"
        ) && !url.matches("(^https?://cl\\.ly/robots\\.txt$)|(^https?://cl\\.ly/image/?$)") && !(url.contains("imgur.com") && url.contains(","));
    }

    public synchronized static ImageList getInstance() {
        if (instance == null)
            instance = new ImageList();
        return instance;
    }

    public ImageList() {
        images = new HashMap<>();
        GIFs = new HashMap<>();
        failedURLs = new ArrayList<>();
        urlInfo = new HashMap<>();
        downloadListeners = new HashMap<>();
        activeDownloads = new ArrayList<>();
    }

    public void clear() {
        images.clear();
        GIFs.clear();
        mDownloadThreadPool.purge();
        downloadListeners.clear();
        activeDownloads.clear();
        urlInfo.clear();
    }

    public void clearFailures() {
        failedURLs.clear();
    }

    public void purge() {
        Context context = IRCCloudApplication.getInstance().getApplicationContext();
        if(context != null) {
            File[] files = new File(context.getCacheDir(), "ImageCache").listFiles();
            if(files != null && files.length > 0) {
                for (File file : files) {
                    try {
                        file.delete();
                    } catch (Exception e) {
                        printStackTraceToCrashlytics(e);
                    }
                }
            }
        }
        clear();
    }

    public void prune() {
        long olde = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7);
        Context context = IRCCloudApplication.getInstance().getApplicationContext();
        if(context != null) {
            File[] files = new File(context.getCacheDir(), "ImageCache").listFiles();
            if(files != null && files.length > 0) {
                for (File file : files) {
                    if (file.lastModified() < olde) {
                        try {
                            file.delete();
                        } catch (Exception e) {
                            printStackTraceToCrashlytics(e);
                        }
                    }
                }
            }
        }
    }

    public boolean isFailedURL(String fileID, int width) {
        try {
            if(NetworkConnection.file_uri_template != null)
                return isFailedURL(new URL(UriTemplate.fromTemplate(NetworkConnection.file_uri_template).set("id", fileID).set("modifiers", "w" + width).expand()));
        } catch (Exception e) {
        }
        return false;
    }

    public boolean isFailedURL(String url) {
        try {
            return isFailedURL(new URL(url));
        } catch (Exception e) {
        }
        return false;
    }

    public boolean isFailedURL(URL url) {
        return failedURLs.contains(MD5(url.toString()));
    }

    public GifDrawable getGIF(URL url) throws OutOfMemoryError, FileNotFoundException {
        if(failedURLs.contains(MD5(url.toString())))
            throw new FileNotFoundException();

        GifDrawable gif = GIFs.get(MD5(url.toString()));
        if(gif != null)
            return gif;

        if(cacheFile(url).exists()) {
            try {
                gif = new GifDrawable(cacheFile(url).getAbsolutePath());
                GIFs.put(MD5(url.toString()), gif);
            } catch (IOException e) {
                failedURLs.add(MD5(url.toString()));
            }
        }
        return gif;
    }

    public GifDrawable getGIF(String fileID) throws OutOfMemoryError, FileNotFoundException {
        try {
            if(NetworkConnection.file_uri_template != null)
                return getGIF(new URL(UriTemplate.fromTemplate(NetworkConnection.file_uri_template).set("id", fileID).expand()));
        } catch (Exception e) {
        }
        return null;
    }

    public GifDrawable getGIF(String fileID, int width) throws OutOfMemoryError, FileNotFoundException {
        try {
            if(NetworkConnection.file_uri_template != null)
                return getGIF(new URL(UriTemplate.fromTemplate(NetworkConnection.file_uri_template).set("id", fileID).set("modifiers", "w" + width).expand()));
        } catch (Exception e) {
        }
        return null;
    }


    public Bitmap getImage(URL url) throws OutOfMemoryError, FileNotFoundException, IOException {
        return getImage(url, 0);
    }

    public Bitmap getImage(URL url, int width) throws OutOfMemoryError, FileNotFoundException, IOException {
        if(failedURLs.contains(MD5(url.toString())))
            throw new FileNotFoundException();

        Bitmap bitmap = images.get(MD5(url.toString()));
        if(bitmap != null)
            return bitmap;

        if(cacheFile(url).exists() && !activeDownloads.contains(url.toString())) {
            if (width > 0) {
                bitmap = loadScaledBitmap(cacheFile(url), width);
            } else {
                if (Build.VERSION.SDK_INT >= 28) {
                    ImageDecoder.Source source = ImageDecoder.createSource(cacheFile(url));
                    bitmap = ImageDecoder.decodeBitmap(source, new ImageDecoder.OnHeaderDecodedListener() {
                        @Override
                        public void onHeaderDecoded(@NonNull ImageDecoder imageDecoder, @NonNull ImageDecoder.ImageInfo imageInfo, @NonNull ImageDecoder.Source source) {
                            if (imageInfo.getSize().getWidth() > 2048) {
                                int ratio = (int) (((float) imageInfo.getSize().getWidth()) / ((float) 2048));
                                imageDecoder.setTargetSampleSize(ratio);
                            }
                        }
                    });
                } else {
                    bitmap = BitmapFactory.decodeFile(cacheFile(url).getAbsolutePath());
                }
            }

            try {
                ExifInterface exif = new ExifInterface(cacheFile(url).getPath());
                int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1);
                if (orientation > 1) {
                    Matrix matrix = new Matrix();
                    switch (orientation) {
                        case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
                            matrix.setScale(-1, 1);
                            break;
                        case ExifInterface.ORIENTATION_ROTATE_180:
                            matrix.setRotate(180);
                            break;
                        case ExifInterface.ORIENTATION_FLIP_VERTICAL:
                            matrix.setRotate(180);
                            matrix.postScale(-1, 1);
                            break;
                        case ExifInterface.ORIENTATION_TRANSPOSE:
                            matrix.setRotate(90);
                            matrix.postScale(-1, 1);
                            break;
                        case ExifInterface.ORIENTATION_ROTATE_90:
                            matrix.setRotate(90);
                            break;
                        case ExifInterface.ORIENTATION_TRANSVERSE:
                            matrix.setRotate(-90);
                            matrix.postScale(-1, 1);
                            break;
                        case ExifInterface.ORIENTATION_ROTATE_270:
                            matrix.setRotate(-90);
                            break;
                    }
                    try {
                        bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
                    } catch (OutOfMemoryError e) {
                        Log.e("IRCCloud", "Out of memory rotating the photo");
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

            if (bitmap != null)
                images.put(MD5(url.toString()), bitmap);
            else
                cacheFile(url).delete();
        }
        return bitmap;
    }

    public Bitmap getImage(String fileID) throws OutOfMemoryError, FileNotFoundException {
        try {
            if(NetworkConnection.file_uri_template != null)
                return getImage(new URL(UriTemplate.fromTemplate(NetworkConnection.file_uri_template).set("id", fileID).expand()));
        } catch (Exception e) {
        }
        return null;
    }

    public Bitmap getImage(String fileID, int width) throws OutOfMemoryError, FileNotFoundException {
        try {
            if(NetworkConnection.file_uri_template != null)
                return getImage(new URL(UriTemplate.fromTemplate(NetworkConnection.file_uri_template).set("id", fileID).set("modifiers", "w" + width).expand()), width);
        } catch (Exception e) {
        }
        return null;
    }

    private void notifyListeners(URL url, Bitmap result) {
        synchronized (downloadListeners) {
            if (downloadListeners.containsKey(url.toString())) {
                for (OnImageFetchedListener listener : downloadListeners.get(url.toString())) {
                    listener.onImageFetched(result);
                }
                downloadListeners.remove(url.toString());
            }
        }
    }

    public static Bitmap loadScaledBitmap(File path, int width) throws FileNotFoundException, IOException, OutOfMemoryError {
        if (Build.VERSION.SDK_INT >= 28) {
            ImageDecoder.Source source = ImageDecoder.createSource(path);
            return ImageDecoder.decodeBitmap(source, new ImageDecoder.OnHeaderDecodedListener() {
                @Override
                public void onHeaderDecoded(@NonNull ImageDecoder imageDecoder, @NonNull ImageDecoder.ImageInfo imageInfo, @NonNull ImageDecoder.Source source) {
                    if (imageInfo.getSize().getWidth() > width) {
                        int ratio = (int)(((float)imageInfo.getSize().getWidth()) / ((float) width));
                        imageDecoder.setTargetSampleSize(ratio);
                    }
                }
            });
        }
        FileInputStream is = new FileInputStream(path);
        BitmapFactory.Options dbo = new BitmapFactory.Options();
        dbo.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(is, null, dbo);
        is.close();

        Bitmap b;
        is = new FileInputStream(path);
        if (dbo.outWidth > width) {
            float ratio = ((float) dbo.outWidth) / ((float) width);
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inSampleSize = (int) ratio;
            b = BitmapFactory.decodeStream(is, null, options);
        } else {
            b = BitmapFactory.decodeStream(is);
        }
        is.close();

        return b;
    }

    public void fetchImage(URL url, OnImageFetchedListener listener) throws FileNotFoundException {
        fetchImage(url, 0, listener, 0);
    }

    public void fetchImage(final URL url, final int width, final OnImageFetchedListener listener, final long maxCacheAge) throws FileNotFoundException {
        if(failedURLs.contains(MD5(url.toString()))) {
            throw new FileNotFoundException();
        }

        if (cacheFile(url).exists() && cacheFile(url).length() > 0) {
            if(maxCacheAge == 0) {
                try {
                    Bitmap bitmap = getImage(url, width);
                    if (bitmap == null)
                        failedURLs.add(MD5(url.toString()));
                    if(listener != null)
                        listener.onImageFetched(bitmap);
                } catch (IOException e) {
                    failedURLs.add(MD5(url.toString()));
                } catch (OutOfMemoryError e) {
                    if(listener != null)
                        listener.onImageFetched(null);
                }
            } else if(System.currentTimeMillis() - cacheFile(url).lastModified() < maxCacheAge) {
                return;
            }
        }

        synchronized (downloadListeners) {
            if (downloadListeners.containsKey(url.toString())) {
                if(listener != null)
                    downloadListeners.get(url.toString()).add(listener);
                return;
            } else {
                ArrayList<OnImageFetchedListener> list = new ArrayList<>();
                if(listener != null)
                    list.add(listener);
                downloadListeners.put(url.toString(), list);
            }
        }

        mDownloadThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    HttpURLConnection conn;

                    Proxy proxy = null;
                    int port = -1;

                    String host = System.getProperty("http.proxyHost", null);
                    try {
                        port = Integer.parseInt(System.getProperty("http.proxyPort", "8080"));
                    } catch (NumberFormatException e) {
                        port = -1;
                    }

                    if (host != null && host.length() > 0 && !host.equalsIgnoreCase("localhost") && !host.equalsIgnoreCase("127.0.0.1") && port > 0) {
                        InetSocketAddress proxyAddr = new InetSocketAddress(host, port);
                        proxy = new Proxy(Proxy.Type.HTTP, proxyAddr);
                    }

                    if (host != null && host.length() > 0 && !host.equalsIgnoreCase("localhost") && !host.equalsIgnoreCase("127.0.0.1") && port > 0) {
                        IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "Requesting image via proxy: " + host);
                    }

                    try {
                        if (url.getProtocol().toLowerCase().equals("https")) {
                            HttpsURLConnection https = (HttpsURLConnection) ((proxy != null) ? url.openConnection(proxy) : url.openConnection(Proxy.NO_PROXY));
                            https.setSSLSocketFactory(TrustKit.getInstance().getSSLSocketFactory(url.getHost()));
                            conn = https;
                        } else {
                            conn = (HttpURLConnection) ((proxy != null) ? url.openConnection(proxy) : url.openConnection(Proxy.NO_PROXY));
                        }
                    } catch (IOException e) {
                        printStackTraceToCrashlytics(e);
                        return;
                    }

                    conn.setConnectTimeout(30000);
                    conn.setReadTimeout(30000);
                    conn.setUseCaches(true);
                    conn.setRequestProperty("User-Agent", NetworkConnection.getInstance().useragent);
                    conn.setIfModifiedSince(cacheFile(url).lastModified());

                    try {
                        ConnectivityManager cm = (ConnectivityManager) IRCCloudApplication.getInstance().getSystemService(Context.CONNECTIVITY_SERVICE);
                        NetworkInfo ni = cm.getActiveNetworkInfo();
                        if (ni != null && ni.getType() == ConnectivityManager.TYPE_WIFI) {
                            IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "Loading via WiFi");
                        } else {
                            IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "Loading via mobile");
                        }
                    } catch (Exception e) {
                        printStackTraceToCrashlytics(e);
                    }

                    try {
                        activeDownloads.add(url.toString());
                        if (conn.getResponseCode() == HttpURLConnection.HTTP_OK && conn.getInputStream() != null) {
                            InputStream is = conn.getInputStream();
                            OutputStream os = new FileOutputStream(cacheFile(url));
                            byte[] buffer = new byte[8192];
                            int len;
                            while ((len = is.read(buffer)) != -1) {
                                os.write(buffer, 0, len);
                            }
                            is.close();
                            os.close();
                            cacheFile(url).setLastModified(System.currentTimeMillis());
                            activeDownloads.remove(url.toString());

                            Bitmap bitmap = getImage(url, width);
                            if (bitmap == null) {
                                android.util.Log.e("IRCCloud", "Failed to load bitmap after download");
                                cacheFile(url).delete();
                                failedURLs.add(MD5(url.toString()));
                            }
                            notifyListeners(url, bitmap);
                        } else if(conn.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
                            cacheFile(url).setLastModified(System.currentTimeMillis());
                            activeDownloads.remove(url.toString());
                            if(maxCacheAge == 0) {
                                Bitmap bitmap = getImage(url, width);
                                if (bitmap == null) {
                                    android.util.Log.e("IRCCloud", "Failed to load bitmap after download");
                                    cacheFile(url).delete();
                                    failedURLs.add(MD5(url.toString()));
                                }
                                notifyListeners(url, bitmap);
                            }
                        } else {
                            android.util.Log.e("IRCCloud", "No input stream");
                            failedURLs.add(MD5(url.toString()));
                            notifyListeners(url, null);
                        }
                    } catch (OutOfMemoryError e) {
                        e.printStackTrace();
                        failedURLs.add(MD5(url.toString()));
                        notifyListeners(url, null);
                    } catch (FileNotFoundException e) {
                        failedURLs.add(MD5(url.toString()));
                        e.printStackTrace();
                        notifyListeners(url, null);
                    } catch (IOException e) {
                        e.printStackTrace();
                        failedURLs.add(MD5(url.toString()));
                        printStackTraceToCrashlytics(e);
                        notifyListeners(url, null);
                    }

                    conn.disconnect();
                    activeDownloads.remove(url.toString());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public void fetchImage(String fileID, final OnImageFetchedListener listener) {
        try {
            if(NetworkConnection.file_uri_template != null)
                fetchImage(new URL(UriTemplate.fromTemplate(NetworkConnection.file_uri_template).set("id", fileID).expand()), listener);
        } catch (Exception e) {
            if (listener != null)
                listener.onImageFetched(null);
        }
    }

    public void fetchImage(String fileID, int width, final OnImageFetchedListener listener) {
        try {
            if(NetworkConnection.file_uri_template != null)
                fetchImage(new URL(UriTemplate.fromTemplate(NetworkConnection.file_uri_template).set("id", fileID).set("modifiers", "w" + width).expand()), listener);
        } catch (Exception e) {
            if (listener != null)
                listener.onImageFetched(null);
        }
    }

    public File cacheFile(URL url) {
        new File(IRCCloudApplication.getInstance().getApplicationContext().getCacheDir(), "ImageCache").mkdirs();
        return new File(new File(IRCCloudApplication.getInstance().getApplicationContext().getCacheDir(), "ImageCache"), MD5(url.toString()));
    }

    public static String MD5(String md5) {
        try {
            java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
            byte[] array = md.digest(md5.getBytes(Charset.forName("UTF-8")));
            StringBuilder sb = new StringBuilder();
            for (byte anArray : array) {
                sb.append(Integer.toHexString((anArray & 0xFF) | 0x100).substring(1, 3));
            }
            return sb.toString();
        } catch (java.security.NoSuchAlgorithmException e) {
        }
        return null;
    }

    private void putImageInfo(ImageURLInfo info) {
        urlInfo.put(info.original_url, info);
    }

    public void fetchImageInfo(String URL, OnImageInfoListener listener) {
        if(urlInfo.containsKey(URL)) {
            listener.onImageInfo(urlInfo.get(URL));
        } else {
            String url = URL;
            String lower = url.toLowerCase().replace("https://", "").replace("http://", "");
            if (lower.startsWith("www.dropbox.com/")) {
                if (lower.startsWith("www.dropbox.com/s/")) {
                    url = url.replace("://www.dropbox.com/s/", "://dl.dropboxusercontent.com/s/");
                } else {
                    url = url + "?dl=1";
                }
            } else if ((lower.startsWith("d.pr/i/") || lower.startsWith("droplr.com/i/")) && !lower.endsWith("+")) {
                url += "+";
            } else if (lower.startsWith("imgur.com/") || lower.startsWith("www.imgur.com/") || lower.startsWith("m.imgur.com/")) {
                String id = url.replace("https://", "").replace("http://", "");
                id = id.substring(id.indexOf("/") + 1);

                if (!id.contains("/") && id.length() > 0) {
                    new ImgurTask("image", URL, listener).execute(id);
                } else if (id.startsWith("gallery/") && id.length() > 8) {
                    new ImgurTask("gallery", URL, listener).execute(id.substring(8));
                } else if (id.startsWith("a/") && id.length() > 2) {
                    new ImgurTask("album", URL, listener).execute(id.substring(2));
                } else if (id.startsWith("t/") && id.length() > 2 && id.substring(2).contains("/")) {
                    id = id.substring(2);
                    new ImgurTask("image", URL, listener).execute(id.substring(id.indexOf("/") + 1));
                } else {
                    listener.onImageInfo(null);
                }
                return;
            } else if (lower.startsWith("i.imgur.com") && (lower.endsWith(".gifv") || lower.endsWith(".gif"))) {
                String id = url.replace("https://", "").replace("http://", "");
                id = id.substring(id.indexOf("/") + 1);
                id = id.substring(0, id.lastIndexOf("."));
                new ImgurTask("image", URL, listener).execute(id);
                return;
            } else if (lower.startsWith("giphy.com/") || lower.startsWith("www.giphy.com/") || lower.startsWith("gph.is/")) {
                if (lower.contains("/gifs/") && lower.lastIndexOf("/") > lower.indexOf("/gifs/") + 6)
                    url = url.substring(0, lower.lastIndexOf("/"));
                new OEmbedTask(URL, listener).execute("https://giphy.com/services/oembed/?url=" + url);
                return;
            } else if (lower.startsWith("flickr.com/") || lower.startsWith("www.flickr.com/")) {
                new OEmbedTask(URL, listener).execute("https://www.flickr.com/services/oembed/?format=json&url=" + url);
                return;
            /*} else if (lower.startsWith("instagram.com/") || lower.startsWith("www.instagram.com/") || lower.startsWith("instagr.am/") || lower.startsWith("www.instagr.am/")) {
                new OEmbedTask(URL, listener).execute("https://api.instagram.com/oembed?url=" + url);
                return;*/
            } else if (lower.startsWith("cl.ly")) {
                new ClLyTask(URL, listener).execute(url);
                return;
            } else if (url.matches(".*/wiki/.*/File:.*")) {
                new WikiTask(URL, listener).execute(url.replaceAll("/wiki/.*/File:", "/w/api.php?action=query&format=json&prop=imageinfo&iiprop=url&titles=File:"));
            } else if (lower.startsWith("leetfiles.com/") || lower.startsWith("www.leetfiles.com/")) {
                url = url.replace("www.", "").replace("leetfiles.com/image/", "i.leetfiles.com/").replace("?id=", "");
            } else if (lower.startsWith("leetfil.es/") || lower.startsWith("www.leetfil.es/")) {
                url = url.replace("www.", "").replace("leetfil.es/image/", "i.leetfiles.com/").replace("?id=", "");
            } else if (lower.startsWith("xkcd.com/") || lower.startsWith("www.xkcd.com/")) {
                new XKCDTask(URL, listener).execute(URL);
                return;
            }

            ImageURLInfo info = new ImageURLInfo();
            info.thumbnail = url;
            info.original_url = URL;
            putImageInfo(info);
            listener.onImageInfo(info);
        }
    }

    public static abstract class OnImageFetchedListener {
        public abstract void onImageFetched(Bitmap image);
    }

    public static abstract class OnImageInfoListener {
        public abstract void onImageInfo(ImageURLInfo info);
    }

    private class OEmbedTask extends AsyncTaskEx<String, Void, String> {
        private String provider = null;
        private String giphy_fallback = null;
        private String title = null;
        private String original_url = null;
        private OnImageInfoListener listener = null;

        public OEmbedTask(String original_url, OnImageInfoListener listener) {
            this.listener = listener;
            this.original_url = original_url;
        }

        @Override
        protected String doInBackground(String... params) {
            try {
                JSONObject o = NetworkConnection.getInstance().fetchJSON(params[0]);
                if (o.has("title"))
                    title = o.getString("title");

                if (o.has("provider_name"))
                    provider = o.getString("provider_name");

                if (provider != null && provider.equals("Giphy") && o.has("image") && o.getString("image").endsWith(".gif"))
                    giphy_fallback = o.getString("image");

                if (provider != null && provider.equals("Instagram"))
                    return o.getString("thumbnail_url");

                if ((provider != null && provider.equals("Giphy")) || o.getString("type").equalsIgnoreCase("photo"))
                    return o.getString("url");
            } catch (Exception e) {
            }
            return null;
        }

        @Override
        protected void onPostExecute(String url) {
            if (url != null) {
                if (provider != null && provider.equals("Giphy")) {
                    new GiphyTask(original_url, listener, giphy_fallback).execute(url.substring(url.indexOf("/gifs/") + 6));
                } else {
                    ImageURLInfo info = new ImageURLInfo();
                    info.thumbnail = url;
                    info.description = title;
                    info.original_url = original_url;
                    putImageInfo(info);
                    listener.onImageInfo(info);
                }
            } else {
                listener.onImageInfo(null);
            }
        }
    }

    public class ImgurTask extends AsyncTaskEx<String, Void, JSONObject> {
        private String type = "gallery";
        private final String REST_URL = "https://api.imgur.com/3/";
        private String original_url = null;
        private String title = null;
        private OnImageInfoListener listener = null;

        public ImgurTask(String type, String original_url, OnImageInfoListener listener) {
            this.type = type;
            this.listener = listener;
            this.original_url = original_url;
        }

        @Override
        protected JSONObject doInBackground(String... params) {
            try {
                HashMap<String, String> headers = new HashMap<>();
                headers.put("Authorization", "Client-ID " + BuildConfig.IMGUR_KEY);
                JSONObject o = NetworkConnection.getInstance().fetchJSON(REST_URL + type + "/" + params[0], headers);
                if (o.getBoolean("success")) {
                    JSONObject data = o.getJSONObject("data");
                    if(data.has("title") && !data.isNull("title") && data.getString("title").length() > 0)
                        title = data.getString("title");
                    if((data.has("images_count") && data.getInt("images_count") == 1) || !data.has("is_album") || !data.getBoolean("is_album")) {
                        if(data.has("is_album") && data.getBoolean("is_album"))
                            data = data.getJSONArray("images").getJSONObject(0);
                        if(data.has("title") && !data.isNull("title") && data.getString("title").length() > 0)
                            title = data.getString("title");
                        return data;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }

        @Override
        protected void onPostExecute(JSONObject data) {
            if (data != null) {
                try {
                    ImageURLInfo info = new ImageURLInfo();
                    info.original_url = original_url;
                    info.title = title;
                    if(data.getBoolean("animated") && data.has("mp4")) {
                        info.thumbnail = data.getString("mp4").replace(".mp4", ".gif");
                        info.mp4 = data.getString("mp4");
                    } else {
                        info.thumbnail = data.getString("link");
                    }
                    if(data.has("description") && !data.isNull("description") && data.getString("description").length() > 0)
                        info.description = data.getString("description");
                    putImageInfo(info);
                    listener.onImageInfo(info);
                } catch (Exception e) {
                    listener.onImageInfo(null);
                }
            } else {
                listener.onImageInfo(null);
            }
        }
    }

    public class GiphyTask extends AsyncTaskEx<String, Void, JSONObject> {
        private String original_url = null;
        private OnImageInfoListener listener = null;
        private String fallback = null;

        public GiphyTask(String original_url, OnImageInfoListener listener, String fallback) {
            this.listener = listener;
            this.original_url = original_url;
            this.fallback = fallback;
        }

        @Override
        protected JSONObject doInBackground(String... params) {
            try {
                HashMap<String, String> headers = new HashMap<>();
                //Request metadata using the Giphy public beta API key from https://giphy.api-docs.io/1.0/welcome/access-and-api-keys
                JSONObject o = NetworkConnection.getInstance().fetchJSON("https://api.giphy.com/v1/gifs/" + params[0] + "?api_key=dc6zaTOxFJmzC", headers);
                if (o.has("data") && o.getJSONObject("data").has("images")) {
                    return o.getJSONObject("data").getJSONObject("images").getJSONObject("original");
                }
            } catch (Exception e) {
            }
            return null;
        }

        @Override
        protected void onPostExecute(JSONObject data) {
            if (data != null) {
                try {
                    ImageURLInfo info = new ImageURLInfo();
                    info.original_url = original_url;
                    info.thumbnail = fallback;
                    if (data.has("mp4") && data.getString("mp4").length() > 0)
                        info.mp4 = data.getString("mp4");
                    if (data.getString("url").endsWith(".gif"))
                        info.thumbnail = data.getString("url");
                    putImageInfo(info);
                    listener.onImageInfo(info);
                } catch (Exception e) {
                    listener.onImageInfo(null);
                }
            } else {
                listener.onImageInfo(null);
            }
        }
    }

    private class ClLyTask extends AsyncTaskEx<String, Void, String> {
        private String original_url = null;
        private OnImageInfoListener listener = null;

        public ClLyTask(String original_url, OnImageInfoListener listener) {
            this.listener = listener;
            this.original_url = original_url;
        }

        @Override
        protected String doInBackground(String... params) {
            try {
                JSONObject o = NetworkConnection.getInstance().fetchJSON(params[0]);
                if (o.getString("item_type").equalsIgnoreCase("image"))
                    return o.getString("content_url");
            } catch (Exception e) {
            }
            return null;
        }

        @Override
        protected void onPostExecute(String url) {
            if (url != null) {
                ImageURLInfo info = new ImageURLInfo();
                info.original_url = original_url;
                info.thumbnail = url;
                putImageInfo(info);
                listener.onImageInfo(info);
            } else {
                listener.onImageInfo(null);
            }
        }
    }

    private class WikiTask extends AsyncTaskEx<String, Void, String> {
        private String original_url = null;
        private OnImageInfoListener listener = null;

        public WikiTask(String original_url, OnImageInfoListener listener) {
            this.listener = listener;
            this.original_url = original_url;
        }

        @Override
        protected String doInBackground(String... params) {
            try {
                JSONObject o = NetworkConnection.getInstance().fetchJSON(params[0]);
                JSONObject pages = o.getJSONObject("query").getJSONObject("pages");
                Iterator<String> i = pages.keys();
                String pageid = i.next();
                return pages.getJSONObject(pageid).getJSONArray("imageinfo").getJSONObject(0).getString("url");
            } catch (Exception e) {
            }
            return null;
        }

        @Override
        protected void onPostExecute(String url) {
            if (url != null) {
                ImageURLInfo info = new ImageURLInfo();
                info.original_url = original_url;
                info.thumbnail = url;
                putImageInfo(info);
                listener.onImageInfo(info);
            } else {
                listener.onImageInfo(null);
            }
        }
    }

    public class XKCDTask extends AsyncTaskEx<String, Void, JSONObject> {
        private String original_url = null;
        private OnImageInfoListener listener = null;

        public XKCDTask(String original_url, OnImageInfoListener listener) {
            this.listener = listener;
            this.original_url = original_url;
        }

        @Override
        protected JSONObject doInBackground(String... params) {
            try {
                return NetworkConnection.getInstance().fetchJSON( params[0] + "/info.0.json");
            } catch (Exception e) {
            }
            return null;
        }

        @Override
        protected void onPostExecute(JSONObject data) {
            if (data != null) {
                try {
                    ImageURLInfo info = new ImageURLInfo();
                    info.original_url = original_url;
                    info.thumbnail = data.getString("img");
                    info.title = data.getString("safe_title");
                    info.description = data.getString("alt");
                    putImageInfo(info);
                    listener.onImageInfo(info);
                } catch (Exception e) {
                    listener.onImageInfo(null);
                }
            } else {
                listener.onImageInfo(null);
            }
        }
    }
} public class NotificationsList {
    @Dao
    public interface NotificationsDao {
        @Query("SELECT COUNT(*) FROM Notification")
        int count();

        @Query("SELECT COUNT(*) FROM Notification WHERE nick != null")
        int unread_count();

        @Query("SELECT * FROM Notification ORDER BY bid,eid ASC")
        List<Notification> getNotifications();

        @Query("SELECT * FROM Notification WHERE bid != :excludeBid AND message_type != 'callerid' AND message_type != 'callerid_success' AND message_type != 'channel_invite' ORDER BY bid,eid ASC")
        List<Notification> getMessageNotifications(int excludeBid);

        @Query("SELECT * FROM Notification WHERE bid != :excludeBid AND (message_type = 'callerid' OR message_type = 'callerid_success' OR message_type = 'channel_invite') ORDER BY bid,eid ASC")
        List<Notification> getOtherNotifications(int excludeBid);

        @Query("SELECT * FROM Notification WHERE eid = :eid LIMIT 1")
        Notification getNotification(long eid);

        @Query("SELECT * FROM Notification_LastSeenEID WHERE bid = :bid LIMIT 1")
        Notification_LastSeenEID getLastSeenEID(int bid);

        @Query("SELECT * FROM Notification_ServerNick WHERE cid = :cid LIMIT 1")
        Notification_ServerNick getServerNick(int cid);

        @Insert(onConflict = OnConflictStrategy.REPLACE)
        void insert(Notification notification);

        @Update
        void update(Notification notification);

        @Update
        void update(List<Notification> notification);

        @Delete
        void delete(Notification notification);

        @Delete
        void delete(List<Notification> notifications);

        @Query("DELETE FROM Notification WHERE bid = :bid")
        void delete(int bid);

        @Query("DELETE FROM Notification WHERE bid = :bid AND eid = :eid")
        void delete(int bid, long eid);

        @Insert(onConflict = OnConflictStrategy.REPLACE)
        void insert(Notification_LastSeenEID lastSeenEID);

        @Update
        void update(Notification_LastSeenEID lastSeenEID);

        @Delete
        void delete(Notification_LastSeenEID lastSeenEID);

        @Query("DELETE FROM Notification_LastSeenEID WHERE bid = :bid")
        void deleteLastSeenEID(int bid);

        @Insert(onConflict = OnConflictStrategy.REPLACE)
        void insert(Notification_ServerNick serverNick);

        @Update
        void update(Notification_ServerNick serverNick);

        @Delete
        void delete(Notification_ServerNick serverNick);

        @Query("DELETE FROM Notification")
        void clearNotifications();

        @Query("DELETE FROM Notification_LastSeenEID")
        void clearLastSeenEIDs();

        @Query("DELETE FROM Notification_ServerNick")
        void clearServerNicks();
    }

    private static NotificationsList instance = null;
    private int excludeBid = -1;
    private static final Timer mNotificationTimer = new Timer("notification-timer");
    private TimerTask mNotificationTimerTask = null;
    private HashSet<Integer> bubbles = new HashSet<>();

    public interface NotificationAddedListener {
        void onNotificationAdded(Notification notification);
    }

    public NotificationAddedListener notificationAddedListener = null;

    public static NotificationsList getInstance() {
        if (instance == null)
            instance = new NotificationsList();
        return instance;
    }

    public List<Notification> getNotifications() {
        return IRCCloudDatabase.getInstance().NotificationsDao().getNotifications();
    }

    public void clear() {
        try {
            for (Notification n : getNotifications()) {
                if(!bubbles.contains(n.getBid())) {
                    NotificationManagerCompat.from(IRCCloudApplication.getInstance().getApplicationContext()).cancel((int) (n.getEid() / 1000));
                    NotificationManagerCompat.from(IRCCloudApplication.getInstance().getApplicationContext()).cancel(n.getBid());
                }
            }
        } catch (Exception e) {
            NetworkConnection.printStackTraceToCrashlytics(e);
        }
        updateTeslaUnreadCount();
        IRCCloudDatabase.getInstance().NotificationsDao().clearNotifications();
    }

    public void clearLastSeenEIDs() {
        IRCCloudDatabase.getInstance().NotificationsDao().clearLastSeenEIDs();
    }

    public long getLastSeenEid(int bid) {
        Buffer b = BuffersList.getInstance().getBuffer(bid);
        if(b != null)
            return b.getLast_seen_eid();
        Notification_LastSeenEID eid = IRCCloudDatabase.getInstance().NotificationsDao().getLastSeenEID(bid);
        if (eid != null)
            return eid.getEid();
        else
            return -1;
    }

    public void updateLastSeenEid(int bid, long eid) {
        Notification_LastSeenEID n = new Notification_LastSeenEID();
        n.setBid(bid);
        n.setEid(eid);
        IRCCloudDatabase.getInstance().NotificationsDao().insert(n);
    }

    public String getServerNick(int cid) {
        Server s = ServersList.getInstance().getServer(cid);
        if(s != null && s.getNick() != null && s.getNick().length() > 0)
            return s.getNick();
        Notification_ServerNick nick = IRCCloudDatabase.getInstance().NotificationsDao().getServerNick(cid);
        if (nick != null)
            return nick.getNick();
        else
            return null;
    }

    public void updateServerNick(int cid, String nick) {
        updateServerNick(cid, nick, getServerAvatarURL(cid), getServerIsSlack(cid));
    }

    public void updateServerNick(int cid, String nick, String avatar_url, boolean isSlack) {
        if(nick != null && nick.length() > 0) {
            Notification_ServerNick n = new Notification_ServerNick();
            n.setCid(cid);
            n.setNick(nick);
            n.setAvatar_url(avatar_url);
            n.setIsSlack(isSlack);
            IRCCloudDatabase.getInstance().NotificationsDao().insert(n);
        }
    }

    public String getServerAvatarURL(int cid) {
        if(PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("avatars-off", false) && PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("avatar-images", false)) {
            Notification_ServerNick nick = IRCCloudDatabase.getInstance().NotificationsDao().getServerNick(cid);
            if (nick != null)
                return nick.getAvatar_url();
        }
        return null;
    }

    public boolean getServerIsSlack(int cid) {
        Server s = ServersList.getInstance().getServer(cid);
        if(s != null)
            return s.isSlack();
        Notification_ServerNick nick = IRCCloudDatabase.getInstance().NotificationsDao().getServerNick(cid);
        if (nick != null)
            return nick.getIsSlack();
        return false;
    }

    public void dismiss(int bid, long eid) {
        Log.d("IRCCloud", "Dismiss bid" + bid + " eid"+eid);
        IRCCloudDatabase.getInstance().NotificationsDao().delete(bid, eid);
        updateTeslaUnreadCount();
    }

    public void addNotification(int cid, int bid, long eid, String from, String message, String chan, String buffer_type, String message_type, String network, String avatar_url) {
        long last_eid = getLastSeenEid(bid);
        if (eid <= last_eid) {
            IRCCloudLog.Log("Refusing to add notification for seen eid: " + eid);
            return;
        }

        Notification n = new Notification();
        n.setBid(bid);
        n.setCid(cid);
        n.setEid(eid);
        n.setNick(from);
        n.setMessage(TextUtils.htmlEncode(ColorFormatter.strip(message).toString()));
        n.setChan(chan);
        n.setBuffer_type(buffer_type);
        n.setMessage_type(message_type);
        n.setNetwork(network);
        n.setAvatar_url(avatar_url);

        IRCCloudDatabase.getInstance().NotificationsDao().insert(n);

        if(notificationAddedListener != null)
            notificationAddedListener.onNotificationAdded(n);
    }

    public void deleteOldNotifications() {
        boolean changed = false, pending = false;
        if (mNotificationTimerTask != null) {
            mNotificationTimerTask.cancel();
            pending = true;
        }

        List<Notification> notifications = getNotifications();
        final ArrayList<Notification> oldNotifications = new ArrayList<>();

        for (Notification n : notifications) {
            long last_seen_eid = getLastSeenEid(n.getBid());
            Buffer b = BuffersList.getInstance().getBuffer(n.getBid());
            if(b != null)
                last_seen_eid = b.getLast_seen_eid();
            if (last_seen_eid == -1 || n.getEid() <= last_seen_eid) {
                if(!bubbles.contains(n.getBid())) {
                    oldNotifications.add(n);
                    NotificationManagerCompat.from(IRCCloudApplication.getInstance().getApplicationContext()).cancel(n.getBid());
                    NotificationManagerCompat.from(IRCCloudApplication.getInstance().getApplicationContext()).cancel((int) (n.getEid() / 1000));
                    changed = true;
                }
            }
        }

        if (changed) {
            IRCCloudDatabase.getInstance().runInTransaction(new Runnable() {
                @Override
                public void run() {
                    IRCCloudDatabase.getInstance().NotificationsDao().delete(oldNotifications);
                }
            });

            updateTeslaUnreadCount();
        }

        if(pending)
            showNotifications(mTicker);
    }

    public void deleteNotificationsForBid(int bid) {
        Log.d("IRCCloud", "Removing all notifications for bid" + bid);
        List<Notification> notifications = getOtherNotifications();

        if (notifications.size() > 0) {
            for (Notification n : notifications) {
                if(!bubbles.contains(n.getBid())) {
                    NotificationManagerCompat.from(IRCCloudApplication.getInstance().getApplicationContext()).cancel((int) (n.getEid() / 1000));
                }
            }
        }
        if(!bubbles.contains(bid)) {
            NotificationManagerCompat.from(IRCCloudApplication.getInstance().getApplicationContext()).cancel(bid);
        }

        IRCCloudDatabase.getInstance().NotificationsDao().delete(bid);
        IRCCloudDatabase.getInstance().NotificationsDao().deleteLastSeenEID(bid);
        updateTeslaUnreadCount();
    }

    public int count() {
        return IRCCloudDatabase.getInstance().NotificationsDao().count();
    }

    public List<Notification> getMessageNotifications() {
        return IRCCloudDatabase.getInstance().NotificationsDao().getMessageNotifications(excludeBid);
    }

    public List<Notification> getOtherNotifications() {
        return IRCCloudDatabase.getInstance().NotificationsDao().getOtherNotifications(excludeBid);
    }

    public Notification getNotification(long eid) {
        return IRCCloudDatabase.getInstance().NotificationsDao().getNotification(eid);
    }

    public void excludeBid(int bid) {
        excludeBid = -1;
        if(!bubbles.contains(bid)) {
            List<Notification> notifications = getOtherNotifications();

            if (notifications.size() > 0) {
                for (Notification n : notifications) {
                    NotificationManagerCompat.from(IRCCloudApplication.getInstance().getApplicationContext()).cancel((int) (n.getEid() / 1000));
                }
            }
            NotificationManagerCompat.from(IRCCloudApplication.getInstance().getApplicationContext()).cancel(bid);
        }
        excludeBid = bid;
    }

    private String mTicker = null;

    public void showNotificationsNow() {
        if(mNotificationTimerTask != null)
            mNotificationTimerTask.cancel();
        mNotificationTimerTask = null;
        showMessageNotifications(mTicker);
        showOtherNotifications();
        mTicker = null;
        updateTeslaUnreadCount();
    }

    public synchronized void showNotifications(String ticker) {
        if (ticker != null)
            mTicker = ColorFormatter.emojify(ticker);

        if (mNotificationTimerTask == null) {
            try {
                TimerTask task = new TimerTask() {
                    @Override
                    public void run() {
                        showNotificationsNow();
                    }

                    @Override
                    public boolean cancel() {
                        mNotificationTimerTask = null;
                        return super.cancel();
                    }
                };
                mNotificationTimer.schedule(task, 5000);
                mNotificationTimerTask = task;
            } catch (Exception e) {
                NetworkConnection.printStackTraceToCrashlytics(e);
            }
        }
    }

    private void showOtherNotifications() {
        String title = "";
        String text = "";
        String ticker;
        NotificationCompat.Action action = null;
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext());
        final List<Notification> notifications = getOtherNotifications();

        int notify_type = Integer.parseInt(prefs.getString("notify_type", "1"));
        boolean notify = false;
        if (notify_type == 1 || (notify_type == 2 && NetworkConnection.getInstance().isVisible()))
            notify = true;

        if (notifications.size() > 0 && notify) {
            for (Notification n : notifications) {
                if (!n.isShown()) {
                    IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "Posting notification for type " + n.getMessage_type());
                    if (n.getMessage_type().equals("callerid")) {
                        title = n.getNetwork();
                        text = n.getNick() + " is trying to contact you";
                        ticker = n.getNick() + " is trying to contact you on " + n.getNetwork();

                        Intent i = new Intent(RemoteInputService.ACTION_REPLY);
                        i.setComponent(new ComponentName(IRCCloudApplication.getInstance().getApplicationContext().getPackageName(), RemoteInputService.class.getName()));
                        i.putExtra("cid", n.getCid());
                        i.putExtra("eid", n.getEid());
                        i.putExtra("chan", n.getChan());
                        i.putExtra("buffer_type", n.getBuffer_type());
                        i.putExtra("network", n.getNetwork());
                        i.putExtra("to", n.getNick());
                        i.putExtra("reply", "/accept " + n.getNick());
                        action = new NotificationCompat.Action(R.drawable.ic_wearable_add, "Accept", PendingIntent.getService(IRCCloudApplication.getInstance().getApplicationContext(), (int)(n.getEid() / 1000), i, Build.VERSION.SDK_INT < 23 ? PendingIntent.FLAG_UPDATE_CURRENT : (PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)));
                    } else if(n.getMessage_type().equals("callerid_success")) {
                        title = n.getNetwork();
                        text = n.getNick() + " has been added to your accept list";
                        ticker = n.getNick() + " has been added to your accept list on " + n.getNetwork();
                        Intent i = new Intent(RemoteInputService.ACTION_REPLY);
                        i.setComponent(new ComponentName(IRCCloudApplication.getInstance().getApplicationContext().getPackageName(), RemoteInputService.class.getName()));
                        i.putExtra("cid", n.getCid());
                        i.putExtra("eid", n.getEid());
                        i.putExtra("chan", n.getChan());
                        i.putExtra("buffer_type", n.getBuffer_type());
                        i.putExtra("network", n.getNetwork());
                        i.putExtra("to", n.getNick());
                        action = new NotificationCompat.Action.Builder(R.drawable.ic_wearable_reply, "Message", PendingIntent.getService(IRCCloudApplication.getInstance().getApplicationContext(), (int)(n.getEid() / 1000), i, Build.VERSION.SDK_INT < 23 ? PendingIntent.FLAG_UPDATE_CURRENT : (PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)))
                                .setAllowGeneratedReplies(true)
                                .addRemoteInput(new RemoteInput.Builder("extra_reply").setLabel("Message to " + n.getNick()).build()).build();
                    } else if(n.getMessage_type().equals("channel_invite")) {
                        title = n.getNetwork();
                        text = n.getNick() + " invited you to join " + n.getChan();
                        ticker = text;
                        try {
                            Intent i = new Intent();
                            i.setComponent(new ComponentName(IRCCloudApplication.getInstance().getApplicationContext().getPackageName(), "com.irccloud.android.MainActivity"));
                            i.setData(Uri.parse(IRCCloudApplication.getInstance().getResources().getString(R.string.IRCCLOUD_SCHEME) + "://cid/" + n.getCid() + "/" + URLEncoder.encode(n.getChan(), "UTF-8")));
                            i.putExtra("eid", n.getEid());
                            action = new NotificationCompat.Action(R.drawable.ic_wearable_add, "Join", PendingIntent.getActivity(IRCCloudApplication.getInstance().getApplicationContext(), (int)(n.getEid() / 1000), i, Build.VERSION.SDK_INT < 23 ? PendingIntent.FLAG_UPDATE_CURRENT : (PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)));
                        } catch (Exception e) {
                            action = null;
                        }
                    } else {
                        title = n.getNick();
                        text = n.getMessage();
                        ticker = n.getMessage();
                        action = null;
                    }
                    if(title != null && text != null)
                        NotificationManagerCompat.from(IRCCloudApplication.getInstance().getApplicationContext()).notify((int) (n.getEid() / 1000), buildNotification(ticker, n.getCid(), n.getBid(), new long[]{n.getEid()}, title, text, 1, null, n.getNetwork(), null, action, AvatarsList.getInstance().getAvatar(n.getCid(), n.getNick(), null).getBitmap(false, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 64, IRCCloudApplication.getInstance().getApplicationContext().getResources().getDisplayMetrics()), false, true), AvatarsList.getInstance().getAvatar(n.getCid(), n.getNick(), null).getBitmap(false, 400, false, false), null));
                }
            }

            IRCCloudDatabase.getInstance().runInTransaction(new Runnable() {
                @Override
                public void run() {
                    IRCCloudDatabase.getInstance().NotificationsDao().delete(notifications);
                }
            });
        }
    }

    private boolean hasTouchWiz() {
        try {
            IRCCloudApplication.getInstance().getApplicationContext().getPackageManager().getPackageInfo("com.sec.android.app.launcher", 0);
            return true;
        } catch (Exception e) {
        }
        return false;
    }

    public void addBubble(int bid) {
        bubbles.add(bid);
    }

    public void removeBubble(int bid) {
        bubbles.remove(bid);
        NotificationManagerCompat.from(IRCCloudApplication.getInstance().getApplicationContext()).cancel(bid);
    }

    public void createChannel(String id, String title, int importance, String group) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext());
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationManager nm = ((NotificationManager)IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE));
            String ringtone = "android.resource://" + IRCCloudApplication.getInstance().getApplicationContext().getPackageName() + "/raw/digit";
            NotificationChannel defaults = nm.getNotificationChannel("highlight");
            NotificationChannel c = new NotificationChannel(id, title, importance);
            if(defaults != null && defaults.getSound() != null) {
                c.setSound(defaults.getSound(), defaults.getAudioAttributes());
            } else {
                if (ringtone.length() > 0)
                    c.setSound(Uri.parse(ringtone), new AudioAttributes.Builder()
                            .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
                            .build());
            }
            if(defaults != null) {
                c.enableLights(defaults.shouldShowLights());
                c.setLightColor(defaults.getLightColor());
                c.enableVibration(defaults.shouldVibrate());
                c.setVibrationPattern(defaults.getVibrationPattern());
            } else {
                c.enableLights(true);
                c.enableVibration(true);
            }
            if(group != null)
                c.setGroup(group);
            nm.createNotificationChannel(c);
        }
    }

    @SuppressLint("NewApi")
    private android.app.Notification buildNotification(String ticker, int cid, int bid, long[] eids, String title, String text, int count, Intent replyIntent, String network, ArrayList<Notification> messages, NotificationCompat.Action otherAction, Bitmap largeIcon, Bitmap wearBackground, HashMap<String, Bitmap> avatars) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext());
        String ringtone = prefs.getString("notify_ringtone", "android.resource://" + IRCCloudApplication.getInstance().getApplicationContext().getPackageName() + "/raw/digit");
        String uid = prefs.getString("uid", "");
        int defaults = 0;
        String channelId = prefs.getBoolean("notify_channels", false) ? (uid + String.valueOf(bid)) : "highlight";
        if(prefs.getBoolean("notify_channels", false))
            createChannel(uid + String.valueOf(bid), title, NotificationManagerCompat.IMPORTANCE_HIGH, String.valueOf(cid));
        NotificationCompat.Builder builder = new NotificationCompat.Builder(IRCCloudApplication.getInstance().getApplicationContext(), channelId)
                .setContentTitle(title + ((network != null && !network.equals(title)) ? (" (" + network + ")") : ""))
                .setContentText(Html.fromHtml(text))
                .setAutoCancel(true)
                .setTicker(ticker)
                .setWhen(eids[0] / 1000)
                .setSmallIcon(R.drawable.ic_stat_notify)
                .setColor(IRCCloudApplication.getInstance().getApplicationContext().getResources().getColor(R.color.ic_background))
                .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
                .setCategory(NotificationCompat.CATEGORY_MESSAGE)
                .setOnlyAlertOnce(ticker == null);

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            builder.setLargeIcon(largeIcon);
        }
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O && ticker != null && (System.currentTimeMillis() - prefs.getLong("lastNotificationTime", 0)) > 2000) {
            if (ringtone.length() > 0)
                builder.setSound(Uri.parse(ringtone));
        }
        if (!hasTouchWiz() && Build.VERSION.SDK_INT < Build.VERSION_CODES.O && ticker != null)
            builder.setPriority(NotificationCompat.PRIORITY_HIGH);

        int led_color = Integer.parseInt(prefs.getString("notify_led_color", "1"));
        if (led_color == 1) {
            defaults = android.app.Notification.DEFAULT_LIGHTS;
        } else if (led_color == 2) {
            builder.setLights(0xFF0000FF, 500, 500);
        }

        if (prefs.getBoolean("notify_vibrate", true) && ticker != null && (System.currentTimeMillis() - prefs.getLong("lastNotificationTime", 0)) > 2000)
            defaults |= android.app.Notification.DEFAULT_VIBRATE;

        builder.setDefaults(defaults);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putLong("lastNotificationTime", System.currentTimeMillis());
        editor.apply();

        Intent i = new Intent(Intent.ACTION_VIEW);
        i.setComponent(new ComponentName(IRCCloudApplication.getInstance().getApplicationContext().getPackageName(), "com.irccloud.android.MainActivity"));
        i.putExtra("bid", bid);
        i.setData(Uri.parse("bid://" + bid));
        Intent dismiss = new Intent(IRCCloudApplication.getInstance().getApplicationContext(), NotificationDismissBroadcastReceiver.class);
        dismiss.setData(Uri.parse("irccloud-dismiss://" + bid));
        dismiss.putExtra("bid", bid);
        dismiss.putExtra("eids", eids);

        PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(IRCCloudApplication.getInstance().getApplicationContext(), 0, dismiss, Build.VERSION.SDK_INT < 31 ? PendingIntent.FLAG_UPDATE_CURRENT : (PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE));

        builder.setContentIntent(PendingIntent.getActivity(IRCCloudApplication.getInstance().getApplicationContext(), 0, i, Build.VERSION.SDK_INT < 23 ? PendingIntent.FLAG_UPDATE_CURRENT : (PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)));
        builder.setDeleteIntent(dismissPendingIntent);

        WearableExtender wearableExtender = new WearableExtender();
        wearableExtender.setBackground(wearBackground);
        if(messages != null && messages.size() > 0) {
            StringBuilder weartext = new StringBuilder();
            HashMap<String, Person> people = new HashMap<>();
            String servernick = getServerNick(messages.get(0).getCid());
            if(servernick != null && servernick.length() > 0) {
                if (avatars != null && avatars.containsKey(servernick)) {
                    people.put(servernick, new Person.Builder().setName(servernick).setIcon(IconCompat.createWithBitmap(avatars.get(servernick))).build());
                } else {
                    people.put(servernick, new Person.Builder().setName(servernick).build());
                }

                try {
                    RecentConversation c = RecentConversationsList.getInstance().getConversation(cid, bid);
                    NotificationCompat.BubbleMetadata.Builder bubbleBuilder = new NotificationCompat.BubbleMetadata.Builder();
                    Intent b = new Intent(Intent.ACTION_VIEW);
                    b.setComponent(new ComponentName(IRCCloudApplication.getInstance().getApplicationContext(), BubbleActivity.class));
                    b.putExtra("bid", bid);
                    b.setData(Uri.parse("bid://" + bid));
                    bubbleBuilder.setIntent(PendingIntent.getActivity(IRCCloudApplication.getInstance().getApplicationContext(), 0, b, Build.VERSION.SDK_INT < 31 ? PendingIntent.FLAG_UPDATE_CURRENT : (PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE)));
                    bubbleBuilder.setDeleteIntent(dismissPendingIntent);
                    bubbleBuilder.setDesiredHeight(4096);
                    bubbleBuilder.setIcon(AvatarsList.getIconForBuffer(c.getBuffer(), null));
                    builder.setBubbleMetadata(bubbleBuilder.build());
                } catch (Exception e) {
                    e.printStackTrace();
                }

                NotificationCompat.MessagingStyle style = new NotificationCompat.MessagingStyle(people.get(servernick));
                style.setConversationTitle(title + ((network != null) ? (" (" + network + ")") : ""));
                style.setGroupConversation(true);

                for (Notification n : messages) {
                    if (n != null && n.getMessage() != null && n.getMessage().length() > 0) {
                        if (weartext.length() > 0)
                            weartext.append("<br/>");
                        String nick = (n.getNick() != null) ? n.getNick() : servernick;
                        Person p = people.get(nick);
                        if (p == null) {
                            if (avatars != null && avatars.containsKey(nick)) {
                                p = new Person.Builder()
                                        .setName(nick)
                                        .setIcon(IconCompat.createWithBitmap(avatars.get(nick)))
                                        .build();
                            } else {
                                p = new Person.Builder()
                                        .setName(nick)
                                        .build();
                            }
                            people.put(nick, p);
                        }
                        if (n.getMessage_type().equals("buffer_me_msg")) {
                            style.addMessage(new NotificationCompat.MessagingStyle.Message(Html.fromHtml("— " + n.getMessage()).toString(), n.getEid() / 1000, p));
                            weartext.append("<b>— ").append((n.getNick() == null) ? servernick : n.getNick()).append("</b> ").append(n.getMessage());
                        } else {
                            style.addMessage(new NotificationCompat.MessagingStyle.Message(Html.fromHtml(n.getMessage()).toString(), n.getEid() / 1000, p));
                            weartext.append("<b>&lt;").append((n.getNick() == null) ? servernick : n.getNick()).append("&gt;</b> ").append(n.getMessage());
                        }
                    }
                }

                builder.setStyle(style);

                RecentConversationsList.getInstance().setPeople(bid, people.values().toArray(new Person[0]));
                RecentConversationsList.getInstance().publishShortcuts();
                builder.setShortcutId(String.valueOf(bid));
            }

            if(messages.size() > 1) {
                wearableExtender.addPage(new NotificationCompat.Builder(IRCCloudApplication.getInstance().getApplicationContext()).setContentText(Html.fromHtml(weartext.toString())).extend(new WearableExtender().setStartScrollBottom(true)).build());
            }

            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
                weartext.setLength(0);
                int j = 0;
                for(Notification n : messages) {
                    if(messages.size() - ++j < 3) {
                        if (n != null && n.getMessage() != null && n.getMessage().length() > 0) {
                            if (weartext.length() > 0)
                                weartext.append("<br/>");
                            if (n.getMessage_type().equals("buffer_me_msg")) {
                                weartext.append("<b>— ").append((n.getNick() == null) ? servernick : n.getNick()).append("</b> ").append(n.getMessage());
                            } else {
                                weartext.append("<b>&lt;").append((n.getNick() == null) ? servernick : n.getNick()).append("&gt;</b> ").append(n.getMessage());
                            }
                        }
                    }
                }

                RemoteViews bigContentView = new RemoteViews(IRCCloudApplication.getInstance().getApplicationContext().getPackageName(), R.layout.notification_expanded);
                bigContentView.setTextViewText(R.id.title, title + (!title.equals(network) ? (" (" + network + ")") : ""));
                bigContentView.setTextViewText(R.id.text, Html.fromHtml(weartext.toString()));
                bigContentView.setImageViewBitmap(R.id.image, largeIcon);
                bigContentView.setLong(R.id.time, "setTime", eids[0] / 1000);
                if (count > 3) {
                    bigContentView.setViewVisibility(R.id.more, View.VISIBLE);
                    bigContentView.setTextViewText(R.id.more, "+" + (count - 3) + " more");
                } else {
                    bigContentView.setViewVisibility(R.id.more, View.GONE);
                }
                if(replyIntent != null && prefs.getBoolean("notify_quickreply", true)) {
                    bigContentView.setViewVisibility(R.id.actions, View.VISIBLE);
                    bigContentView.setViewVisibility(R.id.action_divider, View.VISIBLE);
                    i = new Intent(IRCCloudApplication.getInstance().getApplicationContext(), QuickReplyActivity.class);
                    i.setData(Uri.parse("irccloud-bid://" + bid));
                    i.putExtras(replyIntent);
                    i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    PendingIntent quickReplyIntent = PendingIntent.getActivity(IRCCloudApplication.getInstance().getApplicationContext(), 0, i, Build.VERSION.SDK_INT < 23 ? PendingIntent.FLAG_UPDATE_CURRENT : (PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
                    bigContentView.setOnClickPendingIntent(R.id.action_reply, quickReplyIntent);
                }
                builder.setCustomBigContentView(bigContentView);
            }
        }

        if (replyIntent != null) {
            PendingIntent replyPendingIntent = PendingIntent.getService(IRCCloudApplication.getInstance().getApplicationContext(), bid + 1, replyIntent, Build.VERSION.SDK_INT < 31 ? PendingIntent.FLAG_UPDATE_CURRENT : (PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE));

            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                builder.addAction(new NotificationCompat.Action.Builder(0,
                        "Reply", replyPendingIntent)
                        .setAllowGeneratedReplies(true)
                        .addRemoteInput(new RemoteInput.Builder("extra_reply").setLabel("Reply to " + title).build()).build());
            }

            NotificationCompat.Action.Builder actionBuilder = new NotificationCompat.Action.Builder(R.drawable.ic_wearable_reply,
                    "Reply", replyPendingIntent)
                    .setAllowGeneratedReplies(true)
                    .addRemoteInput(new RemoteInput.Builder("extra_reply").setLabel("Reply to " + title).build());

            NotificationCompat.Action.WearableExtender actionExtender =
                    new NotificationCompat.Action.WearableExtender()
                            .setHintLaunchesActivity(true)
                            .setHintDisplayActionInline(true);

            wearableExtender.addAction(actionBuilder.extend(actionExtender).build());

            NotificationCompat.CarExtender.UnreadConversation.Builder unreadConvBuilder =
                    new NotificationCompat.CarExtender.UnreadConversation.Builder(title + ((network != null) ? (" (" + network + ")") : ""))
                            .setReadPendingIntent(dismissPendingIntent)
                            .setReplyAction(replyPendingIntent, new RemoteInput.Builder("extra_reply").setLabel("Reply to " + title).build());

            if (messages != null) {
                for (Notification n : messages) {
                    if (n != null && n.getNick() != null && n.getMessage() != null && n.getMessage().length() > 0) {
                        if (n.getBuffer_type().equals("conversation")) {
                            if (n.getMessage_type().equals("buffer_me_msg"))
                                unreadConvBuilder.addMessage("— " + n.getNick() + " " + Html.fromHtml(n.getMessage()).toString());
                            else
                                unreadConvBuilder.addMessage(Html.fromHtml(n.getMessage()).toString());
                        } else {
                            if (n.getMessage_type().equals("buffer_me_msg"))
                                unreadConvBuilder.addMessage("— " + n.getNick() + " " + Html.fromHtml(n.getMessage()).toString());
                            else
                                unreadConvBuilder.addMessage(n.getNick() + " said: " + Html.fromHtml(n.getMessage()).toString());
                        }
                    }
                }
            } else {
                unreadConvBuilder.addMessage(text);
            }
            unreadConvBuilder.setLatestTimestamp(eids[count - 1] / 1000);

            builder.extend(new NotificationCompat.CarExtender().setUnreadConversation(unreadConvBuilder.build()));
        }

        if (replyIntent != null && prefs.getBoolean("notify_quickreply", true) && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            i = new Intent(IRCCloudApplication.getInstance().getApplicationContext(), QuickReplyActivity.class);
            i.setData(Uri.parse("irccloud-bid://" + bid));
            i.putExtras(replyIntent);
            i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            PendingIntent quickReplyIntent = PendingIntent.getActivity(IRCCloudApplication.getInstance().getApplicationContext(), 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
            builder.addAction(R.drawable.ic_action_reply, "Quick Reply", quickReplyIntent);
        }

        /*if (replyIntent != null) {
            Intent markAsRead = new Intent(IRCCloudApplication.getInstance().getApplicationContext(), MarkAsReadBroadcastReceiver.class);
            markAsRead.setData(Uri.parse("irccloud-markasread://" + bid));
            markAsRead.putExtra("cid", cid);
            markAsRead.putExtra("bid", bid);
            markAsRead.putExtra("eids", eids);

            PendingIntent pendingIntent = PendingIntent.getBroadcast(IRCCloudApplication.getInstance().getApplicationContext(), 0, markAsRead, Build.VERSION.SDK_INT < 23 ? PendingIntent.FLAG_UPDATE_CURRENT : (PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
            builder.addAction(0, "Mark As Read", pendingIntent);
        }*/

        if(otherAction != null) {
            int drawable = 0;
            if(otherAction.getIcon() == R.drawable.ic_wearable_add)
                drawable = R.drawable.ic_action_add;
            else if(otherAction.getIcon() == R.drawable.ic_wearable_reply)
                drawable = R.drawable.ic_action_reply;
            builder.addAction(new NotificationCompat.Action(drawable, otherAction.getTitle(), otherAction.getActionIntent()));
            wearableExtender.addAction(otherAction);
        }

        builder.extend(wearableExtender);

        return builder.build();
    }

    private synchronized void showMessageNotifications(String ticker) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext());
        String text = "";
        final List<Notification> notifications = getMessageNotifications();
        boolean pref_avatarsOff = !prefs.getBoolean("avatars-off", true);
        boolean pref_avatarImages = prefs.getBoolean("avatar-images", false);

        int notify_type = Integer.parseInt(prefs.getString("notify_type", "1"));
        boolean notify = false;
        if (notify_type == 1 || (notify_type == 2 && NetworkConnection.getInstance().isVisible()))
            notify = true;

        if (notifications.size() > 0 && notify) {
            int lastbid = notifications.get(0).getBid();
            boolean selfOnly = true;
            int count = 0;
            long[] eids = new long[notifications.size()];
            ArrayList<Notification> messages = new ArrayList<>(notifications.size());
            HashMap<String, Bitmap> avatars = new HashMap<>();
            Notification last = notifications.get(0);
            boolean show = false;
            boolean downloading = false;
            Bitmap avatar = null;
            for (Notification n : notifications) {
                if (n.getBid() != lastbid) {
                    if (show) {
                        String title = last.getChan();
                        if (title == null || title.length() == 0)
                            title = last.getNick();
                        if (title == null || title.length() == 0)
                            title = last.getNetwork();

                        Intent replyIntent = new Intent(RemoteInputService.ACTION_REPLY);
                        replyIntent.putExtra("bid", last.getBid());
                        replyIntent.putExtra("cid", last.getCid());
                        replyIntent.putExtra("eids", eids);
                        replyIntent.putExtra("network", last.getNetwork());
                        replyIntent.putExtra("chan", last.getChan());
                        replyIntent.putExtra("buffer_type", last.getBuffer_type());
                        replyIntent.putExtra("to", last.getChan());

                        String body;
                        if (last.getBuffer_type().equals("channel")) {
                            if (last.getMessage_type().equals("buffer_me_msg"))
                                body = "<b>— " + ((last.getNick() != null)? last.getNick() :getServerNick(last.getCid())) + "</b> " + last.getMessage();
                            else
                                body = "<b>&lt;" + ((last.getNick() != null)? last.getNick() :getServerNick(last.getCid())) + "&gt;</b> " + last.getMessage();
                        } else {
                            if (last.getMessage_type().equals("buffer_me_msg"))
                                body = "— " + ((last.getNick() != null)? last.getNick() :getServerNick(last.getCid())) + " " + last.getMessage();
                            else
                                body = last.getMessage();
                        }

                        ArrayList<String> lines = new ArrayList<>(Arrays.asList(text.split("<br/>")));
                        while(lines.size() > 3)
                            lines.remove(0);

                        if (!selfOnly) {
                            try {
                                IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "Posting notification for type " + last.getMessage_type());
                                Bitmap large_avatar = null;

                                if (!pref_avatarsOff && pref_avatarImages) {
                                    if (last.getAvatar_url() != null && last.getAvatar_url().length() > 0) {
                                        try {
                                            URL url = new URL(last.getAvatar_url());
                                            large_avatar = ImageList.getInstance().getImage(url);
                                            if (large_avatar == null) {
                                                downloading = true;
                                                last.setShown(false);
                                                ImageList.getInstance().fetchImage(url, new ImageList.OnImageFetchedListener() {
                                                    @Override
                                                    public void onImageFetched(Bitmap image) {
                                                        if (mNotificationTimerTask == null) {
                                                            try {
                                                                TimerTask task = new TimerTask() {
                                                                    @Override
                                                                    public void run() {
                                                                        showNotificationsNow();
                                                                    }

                                                                    @Override
                                                                    public boolean cancel() {
                                                                        mNotificationTimerTask = null;
                                                                        return super.cancel();
                                                                    }
                                                                };
                                                                mNotificationTimer.schedule(task, 250);
                                                                mNotificationTimerTask = task;
                                                            } catch (Exception e) {
                                                                NetworkConnection.printStackTraceToCrashlytics(e);
                                                            }
                                                        }
                                                    }
                                                });
                                            }
                                        } catch (Exception e1) {
                                        }
                                    }
                                }

                                if (!downloading) {
                                    if (large_avatar == null) {
                                        large_avatar = AvatarsList.getInstance().getAvatar(last.getCid(), ((last.getNick() != null) ? last.getNick() : getServerNick(last.getCid())), null).getBitmap(false, 512, false, false);
                                    }
                                    NotificationManagerCompat.from(IRCCloudApplication.getInstance().getApplicationContext()).notify(lastbid, buildNotification(ticker, last.getCid(), lastbid, eids, title, body, count, replyIntent, last.getNetwork(), messages, null, avatars.get(last.getNick()), large_avatar, avatars));
                                }
                            } catch (Exception e) {
                                IRCCloudLog.LogException(e);
                            }
                        }
                    }
                    lastbid = n.getBid();
                    text = "";
                    count = 0;
                    eids = new long[notifications.size()];
                    show = false;
                    selfOnly = true;
                    messages.clear();
                    avatars.clear();
                }

                if (text.length() > 0)
                    text += "<br/>";
                if (n.getBuffer_type().equals("conversation") && n.getMessage_type().equals("buffer_me_msg"))
                    text += "— " + n.getMessage();
                else if (n.getBuffer_type().equals("conversation"))
                    text += n.getMessage();
                else if (n.getMessage_type().equals("buffer_me_msg"))
                    text += "<b>— " + ((n.getNick() != null)? n.getNick() :getServerNick(n.getCid())) + "</b> " + n.getMessage();
                else
                    text += "<b>" + ((n.getNick() != null)? n.getNick() :getServerNick(n.getCid())) + "</b> " + n.getMessage();

                if (!n.isShown()) {
                    n.setShown(true);
                    show = true;
                }

                if(n.getNick() != null)
                    selfOnly = false;

                avatar = null;
                if(!pref_avatarsOff && pref_avatarImages) {
                    if (n.getAvatar_url() != null && n.getAvatar_url().length() > 0) {
                        try {
                            URL url = new URL(n.getAvatar_url());
                            avatar = ImageList.getInstance().getImage(url);
                            if (avatar == null) {
                                downloading = true;
                                show = false;
                                n.setShown(false);
                                ImageList.getInstance().fetchImage(url, new ImageList.OnImageFetchedListener() {
                                    @Override
                                    public void onImageFetched(Bitmap image) {
                                        showMessageNotifications(null);
                                    }
                                });
                            }
                        } catch (Exception e1) {
                        }
                    }
                }

                if(!downloading && avatar == null) {
                    avatar = AvatarsList.getInstance().getAvatar(n.getCid(), ((n.getNick() != null)? n.getNick() :getServerNick(n.getCid())), null).getBitmap(false, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 64, IRCCloudApplication.getInstance().getApplicationContext().getResources().getDisplayMetrics()), false, true);
                }

                if(avatar != null)
                    avatars.put(((n.getNick() != null)? n.getNick() :getServerNick(n.getCid())), avatar);

                messages.add(n);
                eids[count++] = n.getEid();
                if(n.getNick() != null)
                    last = n;
            }

            if (show && !selfOnly) {
                String title = last.getChan();
                if (title == null || title.length() == 0)
                    title = last.getNetwork();

                Intent replyIntent = new Intent(RemoteInputService.ACTION_REPLY);
                replyIntent.putExtra("bid", last.getBid());
                replyIntent.putExtra("cid", last.getCid());
                replyIntent.putExtra("network", last.getNetwork());
                replyIntent.putExtra("eids", eids);
                replyIntent.putExtra("chan", last.getChan());
                replyIntent.putExtra("buffer_type", last.getBuffer_type());
                replyIntent.putExtra("to", last.getChan());

                String body = "";
                if (last.getBuffer_type().equals("channel")) {
                    if (last.getMessage_type().equals("buffer_me_msg"))
                        body = "<b>— " + ((last.getNick() != null)? last.getNick() :getServerNick(last.getCid())) + "</b> " + last.getMessage();
                    else
                        body = "<b>&lt;" + ((last.getNick() != null)? last.getNick() :getServerNick(last.getCid())) + "&gt;</b> " + last.getMessage();
                } else {
                    if (last.getMessage_type().equals("buffer_me_msg"))
                        body = "— " + ((last.getNick() != null)? last.getNick() :getServerNick(last.getCid())) + " " + last.getMessage();
                    else
                        body = last.getMessage();
                }

                ArrayList<String> lines = new ArrayList<>(Arrays.asList(text.split("<br/>")));
                while(lines.size() > 3)
                    lines.remove(0);

                try {
                    IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "Posting notification for type " + last.getMessage_type());
                    Bitmap large_avatar = null;

                    if(!pref_avatarsOff && pref_avatarImages) {
                        if (last.getAvatar_url() != null && last.getAvatar_url().length() > 0) {
                            try {
                                URL url = new URL(last.getAvatar_url());
                                large_avatar = ImageList.getInstance().getImage(url);
                                if (large_avatar == null) {
                                    downloading = true;
                                    last.setShown(false);
                                    ImageList.getInstance().fetchImage(url, new ImageList.OnImageFetchedListener() {
                                        @Override
                                        public void onImageFetched(Bitmap image) {
                                            showMessageNotifications(null);
                                        }
                                    });
                                }
                            } catch (Exception e1) {
                            }
                        }
                    }

                    if(!downloading) {
                        if (large_avatar == null) {
                            large_avatar = AvatarsList.getInstance().getAvatar(last.getCid(), ((last.getNick() != null)? last.getNick() :getServerNick(last.getCid())), null).getBitmap(false, 512, false, false);
                        }
                        NotificationManagerCompat.from(IRCCloudApplication.getInstance().getApplicationContext()).notify(lastbid, buildNotification(ticker, last.getCid(), lastbid, eids, title, body, count, replyIntent, last.getNetwork(), messages, null, avatars.get(last.getNick()), large_avatar, avatars));
                    }
                } catch (Exception e) {
                    IRCCloudLog.LogException(e);
                }
            }

            IRCCloudDatabase.getInstance().runInTransaction(new Runnable() {
                @Override
                public void run() {
                    IRCCloudDatabase.getInstance().NotificationsDao().update(notifications);
                }
            });
        }
    }

    public NotificationCompat.Builder alert(int bid, String title, String body) {
        IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "Posting alert notification");
        NotificationCompat.Builder builder = new NotificationCompat.Builder(IRCCloudApplication.getInstance().getApplicationContext(), "alert")
                .setContentTitle(title)
                .setContentText(body)
                .setTicker(body)
                .setAutoCancel(true)
                .setColor(IRCCloudApplication.getInstance().getApplicationContext().getResources().getColor(R.color.ic_background))
                .setSmallIcon(R.drawable.ic_stat_notify);

        Intent i = new Intent();
        i.setComponent(new ComponentName(IRCCloudApplication.getInstance().getApplicationContext().getPackageName(), "com.irccloud.android.MainActivity"));
        i.putExtra("bid", bid);
        i.setData(Uri.parse("bid://" + bid));
        builder.setContentIntent(PendingIntent.getActivity(IRCCloudApplication.getInstance().getApplicationContext(), 0, i, Build.VERSION.SDK_INT < 23 ? PendingIntent.FLAG_UPDATE_CURRENT : (PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)));

        NotificationManagerCompat.from(IRCCloudApplication.getInstance().getApplicationContext()).notify(bid, builder.build());

        return builder;
    }

    public void updateTeslaUnreadCount() {
        try {
            IRCCloudApplication.getInstance().getApplicationContext().getPackageManager().getPackageInfo("com.teslacoilsw.notifier", PackageManager.GET_META_DATA);
        } catch (Exception e) {
            return;
        }

        try {
            ContentValues cv = new ContentValues();
            cv.put("tag", IRCCloudApplication.getInstance().getApplicationContext().getPackageManager().getLaunchIntentForPackage(IRCCloudApplication.getInstance().getApplicationContext().getPackageName()).getComponent().flattenToString());
            cv.put("count", IRCCloudDatabase.getInstance().NotificationsDao().unread_count());
            IRCCloudApplication.getInstance().getApplicationContext().getContentResolver().insert(Uri.parse("content://com.teslacoilsw.notifier/unread_count"), cv);
        } catch (IllegalArgumentException ex) {
        } catch (Exception ex) {
            NetworkConnection.printStackTraceToCrashlytics(ex);
        }
    }

    public void addNotificationGroup(int id, String name) {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            ((NotificationManager)IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE))
                    .createNotificationChannelGroup(new NotificationChannelGroup(String.valueOf(id), name));
        }
    }

    public void pruneNotificationChannels() {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationManager nm = ((NotificationManager) IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE));

            createChannel("alert", "Alerts", NotificationManagerCompat.IMPORTANCE_DEFAULT, null);
            createChannel("highlight", "Highlights and PMs", NotificationManagerCompat.IMPORTANCE_HIGH, null);

            for (NotificationChannelGroup c : nm.getNotificationChannelGroups()) {
                try {
                    if (ServersList.getInstance().getServer(Integer.valueOf(c.getId())) == null)
                        nm.deleteNotificationChannelGroup(c.getId());
                } catch(NumberFormatException e) {
                }
            }

            String uid = PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getString("uid", "uid");
            for (NotificationChannel c : nm.getNotificationChannels()) {
                try {
                    if (c.getId().startsWith("uid") && BuffersList.getInstance().getBuffer(Integer.valueOf(c.getId().substring(uid.length()))) == null)
                        nm.deleteNotificationChannel(c.getId());
                } catch(NumberFormatException e) {
                }
                try {
                    if (Integer.valueOf(c.getId()) > 0)
                        nm.deleteNotificationChannelGroup(c.getId());
                } catch(NumberFormatException e) {
                }

                if(!PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("notify_channels", false) && c.getId().startsWith("uid")) {
                    nm.deleteNotificationChannel(c.getId());
                    if(c.getGroup() != null)
                        nm.deleteNotificationChannelGroup(c.getGroup());
                }
            }
        }
    }
} public class RecentConversationsList {
    @Dao
    public interface RecentConversationsDao {
        @Query("SELECT * FROM RecentConversation ORDER BY timestamp DESC")
        List<RecentConversation> getConversations();

        @Query("SELECT * FROM RecentConversation WHERE cid = :cid AND bid = :bid LIMIT 1")
        RecentConversation getConversation(int cid, int bid);

        @Insert(onConflict = OnConflictStrategy.REPLACE)
        void insert(RecentConversation recentConversation);

        @Update
        void update(RecentConversation recentConversation);

        @Delete
        void delete(RecentConversation recentConversation);

        @Query("DELETE FROM RecentConversation")
        void clear();
    }

    private static RecentConversationsList instance = null;
    private HashMap<Integer, Person[]> people = new HashMap<>();
    public void setPeople(int bid, Person[] p) {
        people.put(bid, p);
    }

    public static RecentConversationsList getInstance() {
        if (instance == null)
            instance = new RecentConversationsList();
        return instance;
    }

    public void clear() {
        IRCCloudDatabase.getInstance().RecentConversationsDao().clear();
        ShortcutManagerCompat.removeAllDynamicShortcuts(IRCCloudApplication.getInstance().getApplicationContext());
    }

    public List<RecentConversation> getConversations() {
        return IRCCloudDatabase.getInstance().RecentConversationsDao().getConversations();
    }

    public RecentConversation getConversation(int cid, int bid) {
        return IRCCloudDatabase.getInstance().RecentConversationsDao().getConversation(cid, bid);
    }

    public void deleteConversation(int cid, int bid) {
        RecentConversation c = getConversation(cid,bid);
        if(c != null)
            IRCCloudDatabase.getInstance().RecentConversationsDao().delete(c);
    }

    public void updateConversation(int cid, int bid, String name, String type, long timestamp) {
        RecentConversation c = getConversation(cid, bid);
        if(c != null) {
            if(c.getTimestamp() < timestamp)
                c.setTimestamp(timestamp);
            else
                return;
        } else if(name != null && type != null) {
            c = new RecentConversation();
            c.setCid(cid);
            c.setBid(bid);
            c.setName(name);
            c.setType(type);
            c.setTimestamp(timestamp);
        }
        if(c != null)
            IRCCloudDatabase.getInstance().RecentConversationsDao().insert(c);
    }

    public void prune() {
        List<RecentConversation> conversations = getConversations();
        IRCCloudDatabase.getInstance().beginTransaction();
        for(RecentConversation c : conversations) {
            if(BuffersList.getInstance().getBuffer(c.getBid()) == null || BuffersList.getInstance().getBuffer(c.getBid()).getArchived() == 1) {
                IRCCloudDatabase.getInstance().RecentConversationsDao().delete(c);
            }
        }
        IRCCloudDatabase.getInstance().endTransaction();

        conversations = getConversations();
        IRCCloudDatabase.getInstance().beginTransaction();
        int MAX_SHORTCUTS = ShortcutManagerCompat.getMaxShortcutCountPerActivity(IRCCloudApplication.getInstance().getApplicationContext());
        while(conversations.size() > MAX_SHORTCUTS) {
            RecentConversation last = conversations.get(conversations.size() - 1);
            conversations.remove(last);
            IRCCloudDatabase.getInstance().RecentConversationsDao().delete(last);
        }
        IRCCloudDatabase.getInstance().endTransaction();
    }

    public void publishShortcuts() {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            int MAX_SHORTCUTS = ShortcutManagerCompat.getMaxShortcutCountPerActivity(IRCCloudApplication.getInstance().getApplicationContext());

            HashSet<String> categories = new HashSet<>();
            categories.add("com.irccloud.android.SHARE_TARGET");

            ArrayList<ShortcutInfoCompat> shortcuts = new ArrayList<>();
            List<RecentConversation> conversations = getConversations();
            for(RecentConversation c : conversations) {
                IconCompat avatar = null;

                try {
                    avatar = AvatarsList.getIconForBuffer(c.getBuffer(), new ImageList.OnImageFetchedListener() {
                        @Override
                        public void onImageFetched(Bitmap image) {
                            RecentConversationsList.getInstance().publishShortcuts();
                        }
                    });
                } catch (Exception e) {

                }

                if(avatar != null) {
                    Intent i = new Intent(Intent.ACTION_VIEW);
                    i.setComponent(new ComponentName(IRCCloudApplication.getInstance().getApplicationContext().getPackageName(), "com.irccloud.android.MainActivity"));
                    i.putExtra("bid", c.getBid());

                    ShortcutInfoCompat.Builder builder = new ShortcutInfoCompat.Builder(IRCCloudApplication.getInstance().getApplicationContext(), String.valueOf(c.getBid()))
                            .setShortLabel(c.getName())
                            .setIcon(avatar)
                            .setIntent(i)
                            .setLongLived(true)
                            .setCategories(categories);

                    if (people.containsKey(c.getBid()))
                        builder.setPersons(people.get(c.getBid()));
                    else if (c.getBuffer().isConversation())
                        builder.setPerson(new Person.Builder().setName(c.getBuffer().getDisplayName()).build());

                    shortcuts.add(builder.build());
                }

                if(shortcuts.size() >= MAX_SHORTCUTS)
                    break;
            }

            ShortcutManagerCompat.removeAllDynamicShortcuts(IRCCloudApplication.getInstance().getApplicationContext());
            ShortcutManagerCompat.addDynamicShortcuts(IRCCloudApplication.getInstance().getApplicationContext(), shortcuts);
        }
    }
} public class ServersList {
    private SparseArray<Server> servers;

    private static ServersList instance = null;

    public synchronized static ServersList getInstance() {
        if (instance == null)
            instance = new ServersList();
        return instance;
    }

    public ServersList() {
        servers = new SparseArray<>();
    }

    public void clear() {
        servers.clear();
        //Delete.table(Server.class);
    }

    public void load() {
        /*Cursor c = new Select().all().from(Server.class).query();
        try {
            long start = System.currentTimeMillis();
            ModelAdapter<Server> modelAdapter = FlowManager.getModelAdapter(Server.class);
            if (c != null && c.moveToFirst()) {
                servers = new SparseArray<>(c.getCount());
                do {
                    Server s = modelAdapter.loadFromCursor(c);
                    servers.put(s.getCid(), s);
                    s.updateIgnores(s.raw_ignores);
                } while(c.moveToNext());
                long time = System.currentTimeMillis() - start;
                android.util.Log.i("IRCCloud", "Loaded " + c.getCount() + " servers in " + time + "ms");
            }
        } catch (SQLiteException e) {
            servers.clear();
        } finally {
            if(c != null)
                c.close();
        }*/
    }

    public void save() {
        /*ArrayList<Server> s = new ArrayList<>(servers.size());
        for (int i = 0; i < servers.size(); i++) {
            s.add(servers.valueAt(i));
        }
        TransactionManager.getInstance().saveOnSaveQueue(s);*/
    }

    public Server createServer(int cid, String name, String hostname, int port, String nick, String status, int ssl, String realname, String server_pass, String nickserv_pass, String join_commands, ObjectNode fail_info, String away, JsonNode ignores, int order, String server_realname, String ircserver, int orgId, int avatars_supported, int slack) {
        Server s = getServer(cid);
        if (s == null) {
            s = new Server();
            servers.put(cid, s);
        }
        s.setCid(cid);
        s.setName(name);
        s.setHostname(hostname);
        s.setPort(port);
        s.setNick(nick);
        s.setFrom(nick);
        s.setStatus(status);
        s.setSsl(ssl);
        s.setRealname(realname);
        s.setServer_pass(server_pass);
        s.setNickserv_pass(nickserv_pass);
        s.setJoin_commands(join_commands);
        s.setFail_info(fail_info);
        s.setAway(away);
        s.setUsermask("");
        s.setMode("");
        s.setOrder(order);
        s.setServerRealname(server_realname);
        s.setIRCServer(ircserver);
        s.setOrgId(orgId);
        s.setAvatars_supported(avatars_supported);
        s.setSlack(slack);
        s.isupport = new ObjectMapper().createObjectNode();
        if (s.getName() == null || s.getName().length() == 0)
            s.setName(s.getHostname());
        if (ignores != null)
            s.updateIgnores(ignores);
        return s;
    }

    public int count() {
        return servers.size();
    }

    public void deleteServer(int cid) {
        servers.remove(cid);
    }

    public void deleteAllDataForServer(int cid) {
        Server s = getServer(cid);
        if (s != null) {
            ArrayList<Buffer> buffersToRemove = new ArrayList<Buffer>();

            Iterator<Buffer> i = BuffersList.getInstance().getBuffersForServer(cid).iterator();
            while (i.hasNext()) {
                Buffer b = i.next();
                buffersToRemove.add(b);
            }

            i = buffersToRemove.iterator();
            while (i.hasNext()) {
                Buffer b = i.next();
                BuffersList.getInstance().deleteAllDataForBuffer(b.getBid());
                NotificationsList.getInstance().deleteNotificationsForBid(b.getBid());
            }
            servers.remove(cid);
            //s.delete();
        }
    }

    public synchronized SparseArray<Server> getServers() {
        return servers;
    }

    public Server getServer(int cid) {
        return servers.get(cid);
    }

    public Server getServer(String hostname) {
        for (int i = 0; i < servers.size(); i++) {
            Server s = servers.valueAt(i);
            if (s.getHostname().equalsIgnoreCase(hostname))
                return s;
        }
        return null;
    }

    public Server getServer(String hostname, int port) {
        for (int i = 0; i < servers.size(); i++) {
            Server s = servers.valueAt(i);
            if (s.getHostname().equalsIgnoreCase(hostname) && s.getPort() == port)
                return s;
        }
        return null;
    }

    public Server getServer(String hostname, boolean ssl) {
        for (int i = 0; i < servers.size(); i++) {
            Server s = servers.valueAt(i);
            if (s.getHostname().equalsIgnoreCase(hostname) && ((!ssl && s.getSsl() == 0) || (ssl && s.getSsl() > 0)))
                return s;
        }
        return null;
    }
} @SuppressLint("UseSparseArrays")
public class UsersList {
    private final HashMap<Integer, TreeMap<String, User>> users;
    private HashSet<Integer> loaded_bids = new HashSet<>();
    private Collator collator;

    private static UsersList instance = null;

    public synchronized static UsersList getInstance() {
        if (instance == null)
            instance = new UsersList();
        return instance;
    }

    public UsersList() {
        users = new HashMap<>(1000);
        collator = Collator.getInstance();
        collator.setStrength(Collator.SECONDARY);
        comparator = new AlphanumComparator(collator);
    }

    public synchronized void clear() {
        users.clear();
        //Delete.table(User.class);
    }

    public void load(int bid) {
        /*synchronized (users) {
            Cursor c = new Select().from(User.class).where(Condition.column(User$Table.BID).is(bid)).query();
            try {
                if(loaded_bids.contains(bid))
                    return;
                long start = System.currentTimeMillis();
                ModelAdapter<User> modelAdapter = FlowManager.getModelAdapter(User.class);
                if (c != null && c.moveToFirst()) {
                    android.util.Log.d("IRCCloud", "Loading users for bid" + bid);
                    do {
                        User e = modelAdapter.loadFromCursor(c);
                        if (!users.containsKey(e.bid) || users.get(e.bid) == null)
                            users.put(e.bid, new TreeMap<String, User>(comparator));
                        users.get(e.bid).put(e.nick.toLowerCase(), e);
                    } while (c.moveToNext());
                    long time = System.currentTimeMillis() - start;
                    android.util.Log.i("IRCCloud", "Loaded " + c.getCount() + " users in " + time + "ms");
                    loaded_bids.add(bid);
                }
            } catch (SQLiteException e) {
                NetworkConnection.printStackTraceToCrashlytics(e);
            } finally {
                if(c != null)
                    c.close();
            }
        }*/
    }

    public void save() {
        /*for (int bid : users.keySet()) {
            TreeMap<String, User> e = users.get(bid);
            if(e != null) {
                TransactionManager.getInstance().saveOnSaveQueue(e.values());
            }
        }*/
    }

    public synchronized User createUser(int cid, int bid, String nick, String hostmask, String mode, String ircserver, int away, String display_name) {
        return createUser(cid, bid, nick, hostmask, mode, ircserver, away, display_name, true);
    }

    public synchronized User createUser(int cid, int bid, String nick, String hostmask, String mode, String ircserver, int away, String display_name, boolean find_old) {
        User u = null;
        if (find_old)
            u = getUser(bid, nick);

        if (u == null) {
            u = new User();
        }
        u.cid = cid;
        u.bid = bid;
        u.nick = nick;
        u.display_name = display_name;
        if(u.display_name != null && u.display_name.length() > 0)
            u.nick_lowercase = display_name.toLowerCase();
        else
            u.nick_lowercase = nick.toLowerCase();
        u.hostmask = hostmask;
        u.mode = mode;
        u.ircserver = ircserver;
        u.away = away;
        u.joined = 1;
        TreeMap<String, User> usersList = users.get(bid);
        if (users.get(bid) == null) {
            usersList = new TreeMap<>(comparator);
            users.put(bid, usersList);
        }
        usersList.put(u.nick_lowercase, u);
        return u;
    }

    private AlphanumComparator comparator;

    public synchronized void deleteUser(int bid, String nick) {
        User u = getUser(bid, nick);
        /*if(u != null)
            u.delete();*/
        if (users.containsKey(bid) && users.get(bid) != null)
            users.get(bid).remove(nick.toLowerCase());
    }

    public synchronized void deleteUsersForBuffer(int bid) {
        users.remove(bid);
        //new Delete().from(User.class).where(Condition.column(User$Table.BID).is(bid)).queryClose();
    }

    public synchronized void updateNick(int bid, String old_nick, String new_nick) {
        User u = getUser(bid, old_nick);
        if (u != null) {
            u.nick = new_nick;
            u.nick_lowercase = new_nick.toLowerCase();
            u.old_nick = old_nick;
            users.get(bid).remove(old_nick.toLowerCase());
            users.get(bid).put(new_nick.toLowerCase(), u);
        }
    }

    public synchronized void updateDisplayName(int cid, String nick, String display_name) {
        for(TreeMap<String,User> users : users.values()) {
            for(User u : users.values()) {
                if(u.cid == cid && u.nick.equals(nick)) {
                    users.remove(u.nick_lowercase);
                    u.display_name = display_name;
                    u.nick_lowercase = display_name.toLowerCase();
                    users.put(u.nick_lowercase, u);
                    break;
                }
            }
        }
    }

    public synchronized void updateAway(int cid, String nick, int away) {
        for (Integer bid : users.keySet()) {
            if (users.get(bid) != null && users.get(bid).containsKey(nick.toLowerCase()) && users.get(bid).get(nick.toLowerCase()).cid == cid) {
                User u = users.get(bid).get(nick.toLowerCase());
                if (u != null) {
                    u.away = away;
                }
            }
        }
    }

    public synchronized void updateHostmask(int bid, String nick, String hostmask) {
        User u = getUser(bid, nick);
        if (u != null) {
            u.hostmask = hostmask;
        }
    }

    public synchronized void updateMode(int bid, String nick, String mode) {
        User u = getUser(bid, nick);
        if (u != null) {
            u.mode = mode;
        }
    }

    public synchronized void updateAwayMsg(int cid, String nick, int away, String away_msg) {
        for (Integer bid : users.keySet()) {
            if (users.get(bid) != null && users.get(bid).containsKey(nick.toLowerCase()) && users.get(bid).get(nick.toLowerCase()).cid == cid) {
                User u = users.get(bid).get(nick.toLowerCase());
                if (u != null) {
                    u.away = away;
                    u.away_msg = away_msg;
                }
            }
        }
    }

    public synchronized ArrayList<User> getUsersForBuffer(int bid) {
        load(bid);
        ArrayList<User> list = new ArrayList<>();
        if (users.containsKey(bid) && users.get(bid) != null) {
            for (User u : users.get(bid).values()) {
                list.add(u);
            }
        }
        return list;
    }

    public synchronized User getUser(int bid, String nick) {
        if (nick != null && users.containsKey(bid) && users.get(bid) != null && users.get(bid).containsKey(nick.toLowerCase())) {
            return users.get(bid).get(nick.toLowerCase());
        }
        load(bid);
        if (nick != null && users.containsKey(bid) && users.get(bid) != null && users.get(bid).containsKey(nick.toLowerCase())) {
            return users.get(bid).get(nick.toLowerCase());
        }
        return null;
    }

    public synchronized User findUserOnConnection(int cid, String nick) {
        //return new Select().from(User.class).where(Condition.column(User$Table.CID).is(cid)).and(Condition.column(User$Table.NICK_LOWERCASE).is(nick.toLowerCase())).querySingle();
        for (Integer bid : users.keySet()) {
            if (users.get(bid) != null && users.get(bid).containsKey(nick.toLowerCase()) && users.get(bid).get(nick.toLowerCase()).cid == cid)
                return users.get(bid).get(nick.toLowerCase());
        }
        return null;
    }

    public synchronized String getDisplayName(int cid, String nick) {
        for (Integer bid : users.keySet()) {
            if (users.get(bid) != null) {
                for(User u : users.get(bid).values()) {
                    if(u.cid != cid)
                        break;
                    if(u.nick.equalsIgnoreCase(nick))
                        return u.getDisplayName();
                }
            }
        }
        return nick;
    }
} public class Buffer extends BaseObservable {
    public static final String TYPE_CONSOLE = "console";
    public static final String TYPE_CHANNEL = "channel";
    public static final String TYPE_CONVERSATION = "conversation";
    public static final String TYPE_ARCHIVES_HEADER = "archives_header";
    public static final String TYPE_JOIN_CHANNEL = "join_channel";
    public static final String TYPE_SPAM = "spam";
    public static final String TYPE_COLLAPSED = "collapsed";
    public static final String TYPE_PINNED = "pinned";
    public static final String TYPE_ADD_NETWORK = "add_network";
    public static final String DEFAULT_CHANTYPES = "#&!+";

    private enum Type {
        CONSOLE, CHANNEL, CONVERSATION, ARCHIVES_HEADER, JOIN_CHANNEL, SPAM, COLLAPSED, PINNED, ADD_NETWORK, OTHER
    }

    private Server server = null;
    private ColorScheme colorScheme = ColorScheme.getInstance();

    public Server getServer() {
        if(server == null)
            server = ServersList.getInstance().getServer(this.cid);

        return server;
    }

    private int bid = -1;

    private int cid = -1;

    private long min_eid;

    private long last_seen_eid;

    private long created;

    private String name;

    private String type;

    private int archived;

    private int deferred;

    private int timeout;

    private String away_msg;

    private CharSequence draft;

    private String chan_types;

    private boolean scrolledUp;

    private int scrollPosition;

    private int scrollPositionOffset;

    private int unread;

    private int highlights;

    private int valid = 1;

    private Type type_int;

    private String displayName;

    private boolean serverIsSlack;
    private int isMPDM = -1;

    private boolean muted;

    private boolean showServerSuffix;

    public String toString() {
        return "{cid:" + getCid() + ", bid:" + getBid() + ", name: " + getName() + ", type: " + getType() + ", archived: " + getArchived() + "}";
    }

    public String normalizedName() {
        if(isMPDM())
            return getDisplayName().toLowerCase();

        if (getChan_types() == null || getChan_types().length() < 2) {
            Server s = ServersList.getInstance().getServer(getCid());
            if (s != null && s.CHANTYPES != null && s.CHANTYPES.length() > 0)
                setChan_types(s.CHANTYPES);
            else
                setChan_types(DEFAULT_CHANTYPES);
        }
        return getName().toLowerCase().replaceAll("^[" + getChan_types() + "]+", "");
    }


    public int getBid() {
        return bid;
    }

    public void setBid(int bid) {
        this.bid = bid;
    }

    public int getCid() {
        return cid;
    }

    public void setCid(int cid) {
        this.cid = cid;
    }

    public long getMin_eid() {
        return min_eid;
    }

    public void setMin_eid(long min_eid) {
        this.min_eid = min_eid;
    }

    public long getLast_seen_eid() {
        return last_seen_eid;
    }

    public void setLast_seen_eid(long last_seen_eid) {
        if(this.last_seen_eid < last_seen_eid)
            this.last_seen_eid = last_seen_eid;
    }

    public long getCreated() {
        return created;
    }

    public void setCreated(long created) {
        this.created = created;
    }

    @Bindable
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        this.displayName = null;
        this.isMPDM = -1;
        if(this.bid != -1) {
            BuffersList.getInstance().dirty = true;
        }
    }

    public boolean isMPDM() {
        if(isMPDM == -1) {
            isMPDM = (serverIsSlack && name.matches("#mpdm-(.*)-\\d")) ? 1 : 0;
        }
        return isMPDM == 1;
    }

    public void showServerSuffix(boolean show) {
        this.showServerSuffix = show;
    }

    @Bindable
    public String getServerSuffix() {
        if(showServerSuffix && getServer() != null) {
            String name = getServer().getName();
            if(name == null || name.length() == 0)
                name = getServer().getHostname();
            return " (" + name + ")";
        }
        return "";
    }

    @Bindable
    public String getDisplayName() {
        if(isMPDM()) {
            if (displayName == null) {
                String selfNick = getServer().getNick();
                ArrayList<String> names = new ArrayList<String>(Arrays.asList(name.split("-")));
                names.remove(0);
                names.remove(names.size() - 1);
                for(int i = 0; i < names.size(); i++) {
                    String name = names.get(i);
                    if(name == null || name.length() == 0 || name.equalsIgnoreCase(selfNick)) {
                        names.remove(i);
                        i--;
                    }
                }
                Collator collator = Collator.getInstance();
                collator.setStrength(Collator.SECONDARY);
                Collections.sort(names, new AlphanumComparator(collator));
                displayName = TextUtils.join(", ", names);
            }
            return displayName;
        }
        return name;
    }

    public void setServerIsSlack(boolean serverIsSlack) {
        this.serverIsSlack = serverIsSlack;
        if(this.bid != -1) {
            BuffersList.getInstance().dirty = true;
        }
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
        switch(type) {
            case TYPE_CONSOLE:
                this.type_int = Type.CONSOLE;
                break;
            case TYPE_CHANNEL:
                this.type_int = Type.CHANNEL;
                break;
            case TYPE_CONVERSATION:
                this.type_int = Type.CONVERSATION;
                break;
            case TYPE_ARCHIVES_HEADER:
                this.type_int = Type.ARCHIVES_HEADER;
                break;
            case TYPE_JOIN_CHANNEL:
                this.type_int = Type.JOIN_CHANNEL;
                break;
            case TYPE_SPAM:
                this.type_int = Type.SPAM;
                break;
            case TYPE_COLLAPSED:
                this.type_int = Type.COLLAPSED;
                break;
            case TYPE_PINNED:
                this.type_int = Type.PINNED;
                break;
            case TYPE_ADD_NETWORK:
                this.type_int = Type.ADD_NETWORK;
                break;
            default:
                this.type_int = Type.OTHER;
                break;
        }
    }

    public boolean isConsole() {
        return type_int == Type.CONSOLE;
    }

    @Bindable
    public boolean getIsConsole() {
        return isConsole();
    }

    public boolean isChannel() {
        return type_int == Type.CHANNEL;
    }

    @Bindable
    public boolean getIsChannel() {
        return isChannel();
    }

    public boolean isConversation() {
        return type_int == Type.CONVERSATION;
    }

    @Bindable
    public boolean getIsConversation() {
        return isConversation();
    }

    @Bindable
    public boolean getIsGroupHeading() {
        return isConsole() || type_int == Type.OTHER;
    }

    @Bindable
    public boolean getIsArchivesHeader() {
        return type_int == Type.ARCHIVES_HEADER;
    }

    public int getArchived() {
        return archived;
    }

    public void setArchived(int archived) {
        this.archived = archived;
        if(this.bid != -1) {
            BuffersList.getInstance().dirty = true;
        }
    }

    public boolean isSpam() {
        return type_int == Type.SPAM;
    }

    @Bindable
    public boolean getIsSpam() {
        return isSpam();
    }

    public boolean isCollapsed() {
        return type_int == Type.COLLAPSED;
    }

    @Bindable
    public boolean getIsCollapsed() {
        return isCollapsed();
    }

    public int getDeferred() {
        return deferred;
    }

    public void setDeferred(int deferred) {
        this.deferred = deferred;
    }

    @Bindable
    public int getTimeout() {
        return timeout;
    }

    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    public String getAway_msg() {
        return away_msg;
    }

    public void setAway_msg(String away_msg) {
        this.away_msg = away_msg;
    }

    public CharSequence getDraft() {
        return draft;
    }

    public void setDraft(CharSequence draft) {
        this.draft = draft;
    }

    public String getChan_types() {
        return chan_types;
    }

    public void setChan_types(String chan_types) {
        this.chan_types = chan_types;
    }

    public boolean getScrolledUp() {
        return scrolledUp;
    }

    public void setScrolledUp(boolean scrolledUp) {
        this.scrolledUp = scrolledUp;
    }

    public int getScrollPosition() {
        return scrollPosition;
    }

    public void setScrollPosition(int scrollPosition) {
        this.scrollPosition = scrollPosition;
    }

    public int getScrollPositionOffset() {
        return scrollPositionOffset;
    }

    public void setScrollPositionOffset(int scrollPositionOffset) {
        this.scrollPositionOffset = scrollPositionOffset;
    }

    @Bindable
    public int getUnread() {
        JSONObject channelDisabledMap = null;
        JSONObject bufferDisabledMap = null;
        JSONObject channelEnabledMap = null;
        JSONObject bufferEnabledMap = null;
        NetworkConnection conn = NetworkConnection.getInstance();

        if (conn.getUserInfo() != null && conn.getUserInfo().prefs != null) {
            try {
                if (conn.getUserInfo().prefs.has("channel-disableTrackUnread"))
                    channelDisabledMap = conn.getUserInfo().prefs.getJSONObject("channel-disableTrackUnread");
                if (conn.getUserInfo().prefs.has("buffer-disableTrackUnread"))
                    bufferDisabledMap = conn.getUserInfo().prefs.getJSONObject("buffer-disableTrackUnread");
                if (conn.getUserInfo().prefs.has("channel-enableTrackUnread"))
                    channelEnabledMap = conn.getUserInfo().prefs.getJSONObject("channel-enableTrackUnread");
                if (conn.getUserInfo().prefs.has("buffer-enableTrackUnread"))
                    bufferEnabledMap = conn.getUserInfo().prefs.getJSONObject("buffer-enableTrackUnread");
            } catch (JSONException e1) {
                NetworkConnection.printStackTraceToCrashlytics(e1);
            }
        }

        try {
            if (type.equals(TYPE_CHANNEL) && channelDisabledMap != null && channelDisabledMap.has(String.valueOf(bid)) && channelDisabledMap.getBoolean(String.valueOf(bid))) {
                return 0;
            } else if(bufferDisabledMap != null && bufferDisabledMap.has(String.valueOf(bid)) && bufferDisabledMap.getBoolean(String.valueOf(bid))) {
                return 0;
            } else if (type.equals(TYPE_CHANNEL) && channelEnabledMap != null && channelEnabledMap.has(String.valueOf(bid)) && channelEnabledMap.getBoolean(String.valueOf(bid))) {
                return unread;
            } else if(bufferEnabledMap != null && bufferEnabledMap.has(String.valueOf(bid)) && bufferEnabledMap.getBoolean(String.valueOf(bid))) {
                return unread;
            } else if(!type.equals(TYPE_CONVERSATION) && conn.getUserInfo() != null && conn.getUserInfo().prefs != null && conn.getUserInfo().prefs.has("disableTrackUnread") && conn.getUserInfo().prefs.get("disableTrackUnread") instanceof Boolean && conn.getUserInfo().prefs.getBoolean("disableTrackUnread")) {
                return 0;
            }
        } catch (JSONException e) {
            NetworkConnection.printStackTraceToCrashlytics(e);
        }
        return unread;
    }

    public void setUnread(int unread) {
        this.unread = unread;
    }

    @Bindable
    public int getHighlights() {
        return muted ? 0 : highlights;
    }

    @Bindable
    public String getHighlightsString() {
        return String.valueOf(getHighlights());
    }

    public void setHighlights(int highlights) {
        this.highlights = highlights;
    }

    public boolean isMuted() {
        return muted;
    }

    public void setMuted(boolean muted) {
        this.muted = muted;
    }

    public int getValid() {
        return valid;
    }

    public void setValid(int valid) {
        this.valid = valid;
    }

    /*@Override
    public void save() {
        if(bid != -1)
            super.save();
    }*/

    public boolean isJoined() {
        return ChannelsList.getInstance().getChannelForBuffer(bid) != null;
    }

    @Bindable public String getContentDescription() {
        String extra = "";
        if(getUnread() > 0)
            extra += ", unread";
        if(getHighlights() > 0)
            extra += ", " + getHighlights() + " highlights";

        if(isConsole())
            return "Network " + getServer().getName() + extra;
        else if(isCollapsed() && archived > 0)
            return "Collapsed network. Tap to expand" + extra;
        else if(isCollapsed())
            return "Expanded network. Tap to collapse" + extra;
        else if(isConversation())
            return "Conversation with " + getDisplayName() + extra;
        else if(isChannel())
            return "Channel " + normalizedName() + extra;
        else
            return getName() + extra;
    }

    @Bindable
    public int getTextColor() {
        if (type_int == Type.ARCHIVES_HEADER || type_int == Type.COLLAPSED) {
            return colorScheme.archivesHeadingTextColor;
        } else if (type_int == Type.JOIN_CHANNEL) {
            return R.color.row_label_join;
        } else if (isConsole()) {
            if(getServer() != null && getServer().isFailed())
                return colorScheme.networkErrorColor;
            else if(getServer() == null || !getServer().isConnected())
                return colorScheme.inactiveBufferTextColor;
            else
                return colorScheme.bufferTextColor;
        } else if (getArchived() == 1) {
            return (type_int == Type.CHANNEL) ? colorScheme.archivedChannelTextColor : colorScheme.archivedBufferTextColor;
        } else if (isChannel() && !isJoined()) {
            return colorScheme.inactiveBufferTextColor;
        } else if (isSpam()) {
            return colorScheme.networkErrorColor;
        } else {
            return colorScheme.bufferTextColor;
        }
    }

    @Bindable
    public String getIcon() {
        if (type_int == Type.COLLAPSED) {
            return (archived > 0) ? FontAwesome.PLUS_SQUARE_O : FontAwesome.MINUS_SQUARE_O;
        } else if(isChannel()) {
            Channel c = ChannelsList.getInstance().getChannelForBuffer(bid);
            if(c != null && (c.hasMode("k") || (serverIsSlack && !isMPDM() && c.hasMode("s"))))
                return FontAwesome.LOCK;
        } else if(isSpam()) {
            return FontAwesome.EXCLAMATION_TRIANGLE;
        } else if(type_int == Type.PINNED) {
            return FontAwesome.THUMB_TACK;
        } else if(type_int == Type.ADD_NETWORK) {
            return FontAwesome.PLUS_CIRCLE;
        }
        return null;
    }

    @Bindable
    public int getBackgroundResource() {
        if(isChannel() || isConversation())
            return colorScheme.bufferBackgroundDrawable;
        else if(isSpam())
            return R.drawable.row_failed_bg;
        else if(isConsole())
            return (getServer() != null && getServer().isFailed()) ? R.drawable.row_failed_bg : colorScheme.serverBackgroundDrawable;
        else if(type_int == Type.JOIN_CHANNEL)
            return colorScheme.bufferBackgroundDrawable;
        else if(type_int == Type.COLLAPSED)
            return colorScheme.bufferBackgroundDrawable;
        else if(type_int == Type.ARCHIVES_HEADER)
            return (getArchived() == 0)?colorScheme.bufferBackgroundDrawable:R.drawable.archived_bg_selected;
        else if(type_int == Type.PINNED)
            return colorScheme.bufferBackgroundDrawable;
        else if(type_int == Type.ADD_NETWORK)
            return colorScheme.bufferBackgroundDrawable;
        else
            return colorScheme.serverBackgroundDrawable;
    }

    @Bindable
    public int getSelectedBackgroundResource() {
        if(isConsole())
            return (getServer() != null && getServer().isFailed()) ? R.drawable.status_fail_bg : colorScheme.selectedBackgroundDrawable;
        else if(type_int == Type.COLLAPSED)
            return colorScheme.bufferBackgroundDrawable;
        else
            return colorScheme.selectedBackgroundDrawable;
    }

    @Bindable
    public int getSelectedBorderResource() {
        if(isConsole())
            return (getServer() != null && getServer().isFailed()) ? R.drawable.status_fail_bg : colorScheme.selectedBorderDrawable;
        else
            return colorScheme.selectedBorderDrawable;
    }

    @Bindable
    public int getBorderResource() {
        if(isConsole())
            return (getServer() != null && getServer().isFailed()) ? R.drawable.networkErrorBorder : colorScheme.serverBorderDrawable;
        else
            return colorScheme.bufferBorderDrawable;
    }

    @Bindable
    public int getSelectedTextColor() {
        if(type_int == Type.ARCHIVES_HEADER)
            return colorScheme.selectedArchivesHeadingColor;
        else
            return colorScheme.selectedBufferTextColor;
    }

    @Bindable
    public int getUnreadColor() { return isCollapsed() ? colorScheme.collapsedBorderDrawable : R.drawable.row_unread_border; }

    @Bindable
    public boolean getShowSpinner() {
        if(type_int == Type.ARCHIVES_HEADER)
            return (getServer() != null && getServer().deferred_archives > 0);
        else
            return (isConsole() && getServer() != null)? getServer().isConnecting() : (this.timeout > 0);
    }

    @Bindable
    public int getSpamIconColor() {
        return 0xffff6e00;
    }
} public class Channel extends BaseObservable {
    public static class Mode {
        public String mode;
        public String param;
    }

    public int cid;

    public int bid;

    public String name;

    public String topic_text;

    public long topic_time;

    public String topic_author;

    public String type;

    public String mode;

    public long timestamp;

    public String url;

    public boolean key;

    public ArrayList<Mode> modes = new ArrayList<>();
    public int valid = 1;

    public synchronized void addMode(String mode, String param, boolean init) {
        if (!init)
            removeMode(mode);
        if (mode.equals("k"))
            key = true;
        Mode m = new Mode();
        m.mode = mode;
        m.param = param;
        modes.add(m);
    }

    public synchronized void removeMode(String mode) {
        if (mode.equals("k"))
            key = false;
        for (Mode m : modes) {
            if (m.mode.equals(mode)) {
                modes.remove(m);
                return;
            }
        }
    }

    public synchronized boolean hasMode(String mode) {
        for (Mode m : modes) {
            if (m.mode.equals(mode)) {
                return true;
            }
        }
        return false;
    }

    public synchronized String paramForMode(String mode) {
        for (Mode m : modes) {
            if (m.mode.equals(mode)) {
                return m.param;
            }
        }
        return null;
    }

    public Buffer getBuffer() {
        return BuffersList.getInstance().getBuffer(bid);
    }
} public class Event {
    public int cid;

    public int bid;

    public long eid;

    public long server_time;

    public String type;

    public String msg;

    public String hostmask;

    //@Column(name = "event_from")
    public String from;

    public String from_nick;

    public String from_mode;

    public String from_realname;

    public String from_hostmask;

    public String nick;

    public String old_nick;

    public String server;

    public String diff;

    public String chan;

    public boolean highlight;

    public boolean self;

    public boolean to_chan;

    public boolean to_buffer;

    public int color;

    public int bg_color;

    public JsonNode ops;

    public long group_eid;

    public int row_type = 0;

    public String group_msg;

    public boolean linkify = true;

    public boolean linkified;

    public String target_mode;

    public int reqid;

    public boolean pending;

    public boolean failed;

    public String command;

    public int day = -1;

    public Spanned contentDescription;

    public JsonNode entities;

    public String avatar;

    public String avatar_url;

    public String msgid;

    public String timestamp;
    public String html;
    public String html_prefix;
    public Spanned formatted;
    public Spanned formatted_nick;
    public Spanned formatted_realname;
    public TimerTask expiration_timer;
    public boolean header;
    public boolean quoted;
    public boolean code_block;
    public long parent_eid;
    public boolean is_reply;
    public int reply_count;
    public HashSet<String> reply_nicks;
    public int mention_offset;
    public boolean ready_for_display;
    public long lastEditEID;
    public boolean edited;

    public Event() {

    }

    public Event(Event e) {
        cid = e.cid;
        bid = e.bid;
        eid = e.eid;
        server_time = e.server_time;
        type = e.type;
        msg = e.msg;
        hostmask = e.hostmask;
        from = e.from;
        from_mode = e.from_mode;
        from_realname = e.from_realname;
        from_hostmask = e.from_hostmask;
        nick = e.nick;
        old_nick = e.old_nick;
        server = e.server;
        diff = e.diff;
        chan = e.chan;
        highlight = e.highlight;
        self = e.self;
        to_chan = e.to_chan;
        to_buffer = e.to_buffer;
        color = e.color;
        bg_color = e.bg_color;
        ops = e.ops;
        group_eid = e.group_eid;
        row_type = e.row_type;
        group_msg = e.group_msg;
        linkify = e.linkify;
        target_mode = e.target_mode;
        reqid = e.reqid;
        pending = e.pending;
        failed = e.failed;
        command = e.command;
        day = e.day;
        contentDescription = e.contentDescription;
        entities = e.entities;
    }

    public String toString() {
        return "{" +
                "cid: " + cid +
                ", bid: " + bid +
                ", eid: " + eid +
                ", type: " + type +
                ", timestamp: " + timestamp +
                ", from: " + from +
                ", hostmask: " + hostmask +
                ", msg: " + msg +
                ", html: " + html +
                ", formatted: " + formatted +
                ", group_eid: " + group_eid +
                ", group_msg: " + group_msg +
                ", pending: " + pending +
                ", self: " + self +
                ", header: " + header +
                ", avatar: " + avatar +
                ", avatar_url: " + avatar_url +
                ", getAvatarURL: " + getAvatarURL(72) +
                "}";
    }

    public long getTime() {
        if(server_time > 0)
            return server_time;
        else
            return (eid / 1000) + NetworkConnection.getInstance().clockOffset;
    }

    public synchronized boolean isImportant(String buffer_type) {
        if (self)
            return false;
        if (type == null) {
            return false;
        }

        Server s = ServersList.getInstance().getServer(cid);
        if (s != null) {
            Ignore ignore = s.ignores;
            String from = this.from_nick;
            if (from == null || from.length() == 0)
                from = this.nick;
            if (ignore.match(from + "!" + hostmask))
                return false;
        }

        if (type.equals("notice") || type.equalsIgnoreCase("channel_invite")) {
            // Notices sent from the server (with no nick sender) aren't important
            // e.g. *** Looking up your hostname...
            if (from == null || from.length() == 0)
                return false;

            // Notices and invites sent to a buffer shouldn't notify in the server buffer
            if (buffer_type.equalsIgnoreCase("console") && (to_chan || to_buffer))
                return false;
        }
        return isMessage();
    }

    public boolean isMessage() {
        return (type.equals("buffer_msg") ||
                type.equals("buffer_me_msg") ||
                type.equals("notice") ||
                type.equals("channel_invite") ||
                type.equals("callerid") ||
                type.equals("wallops"));
    }

    private String cachedAvatarURL;
    private int cachedAvatarSize;

    public String getGravatar(int size) {
        if(from_realname != null && from_realname.length() > 0) {
            Matcher m = Patterns.EMAIL_ADDRESS.matcher(from_realname);
            if(m.find()) {
                String email = from_realname.substring(m.start(), m.end()).toLowerCase();
                try {
                    cachedAvatarURL = "https://www.gravatar.com/avatar/" + ImageList.MD5(email).toLowerCase() + "?size=" + size + "&default=404";
                    if(ImageList.getInstance().isFailedURL(cachedAvatarURL))
                        cachedAvatarURL = null;
                    else
                        cachedAvatarSize = size;
                } catch (Exception e) {
                    cachedAvatarURL = null;
                }
            }
        }
        return cachedAvatarURL;
    }

    public String getAvatarURL(int size) {
        Server s = ServersList.getInstance().getServer(cid);
        Buffer b = BuffersList.getInstance().getBuffer(bid);
        return getAvatarURL(size, b != null && b.isChannel(), s != null && s.isSlack());
    }

    public String getAvatarURL(int size, boolean isChannel, boolean isSlack) {
        boolean isIRCCloudAvatar = false;
        if(!BuildConfig.ENTERPRISE && isMessage() && size != cachedAvatarSize) {
            cachedAvatarURL = null;
            if (avatar != null && avatar.length() > 0) {
                if (NetworkConnection.avatar_uri_template != null)
                    cachedAvatarURL = UriTemplate.fromTemplate(NetworkConnection.avatar_uri_template).set("id", avatar).set("modifiers", "s" + size).expand();
            } else if (avatar_url != null && avatar_url.length() > 0 && avatar_url.startsWith("https://")) {
                cachedAvatarURL = avatar_url;
                if (cachedAvatarURL.contains("{size}")) {
                    if(size <= 72)
                        cachedAvatarURL = UriTemplate.fromTemplate(cachedAvatarURL).set("size", "72").expand();
                    else if(size <= 192)
                        cachedAvatarURL = UriTemplate.fromTemplate(cachedAvatarURL).set("size", "192").expand();
                    else
                        cachedAvatarURL = UriTemplate.fromTemplate(cachedAvatarURL).set("size", "512").expand();
                }
            } else if (NetworkConnection.avatar_redirect_uri_template != null) {
                if (hostmask != null && hostmask.length() > 0 && hostmask.contains("@")) {
                    String ident = hostmask.substring(0, hostmask.indexOf("@"));
                    if(ident.startsWith("~"))
                        ident = ident.substring(1);
                    if (ident.startsWith("uid") || ident.startsWith("sid")) {
                        ident = ident.substring(3);
                        try {
                            if (Integer.valueOf(ident) > 0) {
                                cachedAvatarURL = UriTemplate.fromTemplate(NetworkConnection.avatar_redirect_uri_template).set("id", ident).set("modifiers", "s" + size).expand();
                                isIRCCloudAvatar = true;
                            }
                        } catch (NumberFormatException e) {
                            cachedAvatarURL = null;
                            isIRCCloudAvatar = false;
                        }
                    }
                }
            }

            if (cachedAvatarURL != null && avatar == null && !isIRCCloudAvatar && !isSlack) {
                JSONObject prefs = NetworkConnection.getInstance().getUserInfo().prefs;
                boolean pref_inlineImages = false;

                try {
                    if (prefs.has("inlineimages") && prefs.get("inlineimages") instanceof Boolean && prefs.getBoolean("inlineimages")) {
                        JSONObject inlineImagesMap = null;
                        if (isChannel) {
                            if (prefs.has("channel-inlineimages-disable"))
                                inlineImagesMap = prefs.getJSONObject("channel-inlineimages-disable");
                        } else {
                            if (prefs.has("buffer-inlineimages-disable"))
                                inlineImagesMap = prefs.getJSONObject("buffer-inlineimages-disable");
                        }

                        pref_inlineImages = !(inlineImagesMap != null && inlineImagesMap.has(String.valueOf(bid)) && inlineImagesMap.getBoolean(String.valueOf(bid)));
                    } else {
                        JSONObject inlineImagesMap = null;
                        if (isChannel) {
                            if (prefs.has("channel-inlineimages"))
                                inlineImagesMap = prefs.getJSONObject("channel-inlineimages");
                        } else {
                            if (prefs.has("buffer-inlineimages"))
                                inlineImagesMap = prefs.getJSONObject("buffer-inlineimages");
                        }

                        pref_inlineImages = (inlineImagesMap != null && inlineImagesMap.has(String.valueOf(bid)) && inlineImagesMap.getBoolean(String.valueOf(bid)));
                    }
                } catch (Exception e) {
                    NetworkConnection.printStackTraceToCrashlytics(e);
                }
                if (!pref_inlineImages)
                    cachedAvatarURL = null;
            }
        }
        if(cachedAvatarURL == null || ImageList.getInstance().isFailedURL(cachedAvatarURL))
            cachedAvatarURL = getGravatar(size);
        if(cachedAvatarURL != null)
            cachedAvatarSize = size;
        return cachedAvatarURL;
    }

    public String reply = null;
    public String reply() {
        if(reply != null)
            return reply;
        if(entities != null && entities.has("reply") && !entities.get("reply").isNull())
            return entities.get("reply").asText();
        if(entities != null && entities.has("known_client_tags") && !entities.get("known_client_tags").isNull() && entities.get("known_client_tags").has("reply"))
            return entities.get("known_client_tags").get("reply").asText();
        return null;
    }
} public class Server extends BaseObservable implements Comparable<Server> {
    private int cid = -1;

    private String name;

    private String hostname;

    private int port;

    private String nick;

    private String from;

    private String avatar;

    private String avatar_url;

    private String status;

    private int ssl;

    private String realname;

    private String server_realname;

    private ObjectNode fail_info;

    private String away;

    private String usermask;

    private String mode;

    private String ircserver;

    private int orgId;

    private int avatars_supported;

    private int slack;

    public ObjectNode isupport = new ObjectMapper().createObjectNode();

    public JsonNode raw_ignores;

    //@Column(name = "server_order")
    private int order;

    public String CHANTYPES;

    public ObjectNode PREFIX;

    public String MODE_OPER = "Y";

    public String MODE_OWNER = "q";

    public String MODE_ADMIN = "a";

    public String MODE_OP = "o";

    public String MODE_HALFOP = "h";

    public String MODE_VOICED = "v";

    private String server_pass;
    private String nickserv_pass;
    private String join_commands;
    public Ignore ignores = new Ignore();
    public int deferred_archives;
    private int isSlack = -1;
    public Buffer collapsed;

    public boolean blocksTyping = false;
    public boolean blocksReplies = false;
    public boolean blocksReactions = false;
    public boolean blocksEdits = false;
    public boolean blocksDeletes = false;

    @Override
    public int compareTo(Server another) {
        if (getOrder() != another.getOrder())
            return Integer.valueOf(getOrder()).compareTo(another.getOrder());
        return Integer.valueOf(getCid()).compareTo(another.getCid());
    }

    public int getCid() {
        return cid;
    }

    public void setCid(int cid) {
        this.cid = cid;
    }

    @Bindable
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Bindable
    public String getHostname() {
        return hostname;
    }

    public void setHostname(String hostname) {
        this.hostname = hostname;
        isSlack = -1;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public String getNick() {
        return nick;
    }

    public void setNick(String nick) {
        this.nick = nick;
    }

    public String getFrom() {
        return from;
    }

    public void setFrom(String from) {
        this.from = from;
    }

    public String getAvatar() {
        return avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

    public String getAvatarURL() {
        return avatar_url;
    }

    public void setAvatarURL(String avatar_url) {
        this.avatar_url = avatar_url;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public ObjectNode getFail_info() {
        return fail_info;
    }

    public void setFail_info(ObjectNode fail_info) {
        this.fail_info = fail_info;
    }

    public boolean isConnecting() {
        return this.status.equals("waiting_to_retry") || this.status.equals("queued") || this.status.equals("connecting") || this.status.equals("connected_joining");
    }

    @Bindable
    public boolean getIsConnecting() {
        return isConnecting();
    }

    public boolean isConnected() {
        return this.status.equals("connected_ready");
    }

    @Bindable
    public boolean getIsConnected() {
        return isConnected();
    }

    public boolean isFailed() {
        return (this.status.equals("waiting_to_retry") || this.status.equals("pool_unavailable") || (this.status.equals("disconnected") && this.getFail_info() != null && this.getFail_info().has("type")));
    }

    @Bindable
    public boolean getIsFailed() {
        return isFailed();
    }

    public int getSsl() {
        return ssl;
    }

    public void setSsl(int ssl) {
        this.ssl = ssl;
    }

    public String getRealname() {
        return realname;
    }

    public void setRealname(String realname) {
        this.realname = realname;
    }

    public String getServerRealname() {
        return server_realname;
    }

    public void setServerRealname(String server_realname) {
        this.server_realname = server_realname;
    }

    public String getAway() {
        return away;
    }

    public void setAway(String away) {
        this.away = away;
    }

    public String getUsermask() {
        return usermask;
    }

    public void setUsermask(String usermask) {
        this.usermask = usermask;
    }

    public String getMode() {
        return mode;
    }

    public void setMode(String mode) {
        this.mode = mode;
    }

    public int getOrder() {
        return order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    public String getServer_pass() {
        return server_pass;
    }

    public void setServer_pass(String server_pass) {
        this.server_pass = server_pass;
    }

    public String getNickserv_pass() {
        return nickserv_pass;
    }

    public void setNickserv_pass(String nickserv_pass) {
        this.nickserv_pass = nickserv_pass;
    }

    public String getJoin_commands() {
        return join_commands;
    }

    public void setJoin_commands(String join_commands) {
        this.join_commands = join_commands;
    }

    public String getIRCServer() {
        return ircserver;
    }

    public void setIRCServer(String ircserver) {
        this.ircserver = ircserver;
        isSlack = -1;
    }

    public int getOrgId() {
        return orgId;
    }

    public void setOrgId(int orgId) {
        this.orgId = orgId;
    }

    public int getAvatars_supported() {
        return avatars_supported;
    }

    public void setAvatars_supported(int avatars_supported) {
        this.avatars_supported = avatars_supported;
    }

    public int getSlack() {
        return slack;
    }

    public void setSlack(int slack) {
        this.slack = slack;
    }

    public void updateUserModes(String modes) {
        if (modes != null && modes.length() > 0) {
            if(isupport != null && isupport.has("OWNER") && isupport.get("OWNER").isTextual() && isupport.get("OWNER").asText().equals(modes.substring(0,1))) {
                MODE_OWNER = modes.substring(0, 1);
                if(MODE_OPER.equalsIgnoreCase(MODE_OWNER))
                    MODE_OPER = "";
            }
        }
    }

    public boolean clientTagDeny(String tagname) {
        if(isupport == null || !isupport.has("CLIENTTAGDENY") || !isupport.get("CLIENTTAGDENY").isTextual())
            return false;

        boolean denied = false;

        String[] tags = isupport.get("CLIENTTAGDENY").asText().split(",");
        for(String tag : tags) {
            if(tag.equals("*") || tag.equals(tagname))
                denied = true;
            if(tag.equals("~" + tagname))
                denied = false;
        }

        return denied;
    }

    public void updateIsupport(ObjectNode params) {
        if(isupport == null)
            isupport = new ObjectMapper().createObjectNode();

        if (params != null && !params.isArray())
            isupport.putAll(params);
        else
            isupport = new ObjectMapper().createObjectNode();

        if (isupport.has("PREFIX") && isupport.get("PREFIX").isObject()) {
            PREFIX = (ObjectNode) isupport.get("PREFIX");
        } else {
            PREFIX = new ObjectMapper().createObjectNode();
            PREFIX.put(MODE_OPER, "!");
            PREFIX.put(MODE_OWNER, "~");
            PREFIX.put(MODE_ADMIN, "&");
            PREFIX.put(MODE_OP, "@");
            PREFIX.put(MODE_HALFOP, "%");
            PREFIX.put(MODE_VOICED, "+");
        }
        if (isupport.has("CHANTYPES") && isupport.get("CHANTYPES").isTextual())
            CHANTYPES = isupport.get("CHANTYPES").asText();
        else
            CHANTYPES = Buffer.DEFAULT_CHANTYPES;

        for(Buffer b : BuffersList.getInstance().getBuffersForServer(cid)) {
            b.setChan_types(CHANTYPES);
        }

        blocksTyping = clientTagDeny("typing") && clientTagDeny("draft/typing");
        blocksReplies = clientTagDeny("draft/reply");
        blocksReactions = blocksReplies || clientTagDeny("draft/react");
        blocksEdits = clientTagDeny("draft/edit") || clientTagDeny("draft/edit-text");
        blocksDeletes = clientTagDeny("draft/delete");
    }

    public void updateIgnores(JsonNode ignores) {
        this.raw_ignores = ignores;
        this.ignores.clear();
        if(ignores != null) {
            for (int i = 0; i < ignores.size(); i++) {
                String mask = ignores.get(i).asText().toLowerCase()
                        .replace("\\", "\\\\")
                        .replace("(", "\\(")
                        .replace(")", "\\)")
                        .replace("[", "\\[")
                        .replace("]", "\\]")
                        .replace("{", "\\{")
                        .replace("}", "\\}")
                        .replace("-", "\\-")
                        .replace("^", "\\^")
                        .replace("$", "\\$")
                        .replace("|", "\\|")
                        .replace("+", "\\+")
                        .replace("?", "\\?")
                        .replace(".", "\\.")
                        .replace(",", "\\,")
                        .replace("#", "\\#")
                        .replace("*", ".*")
                        .replace("!~", "!");
                if (!mask.contains("!"))
                    if (mask.contains("@"))
                        mask = ".*!" + mask;
                    else
                        mask += "!.*";
                if (!mask.contains("@"))
                    if (mask.contains("!"))
                        mask = mask.replace("!", "!.*@");
                    else
                        mask += "@.*";
                if (mask.equals(".*!.*@.*"))
                    continue;
                this.ignores.addMask(mask);
            }
        }
    }

    @Bindable
    public String getIcon() {
        if(isSlack())
            return FontAwesome.SLACK;
        else if(ssl > 0)
            return FontAwesome.SHIELD;
        else
            return FontAwesome.GLOBE;
    }

    public boolean isSlack() {
        if(isSlack == -1)
            isSlack = (slack == 1 || (hostname != null && hostname.endsWith(".slack.com")) || (ircserver != null && ircserver.endsWith(".slack.com"))) ? 1 : 0;
        return isSlack == 1;
    }

    public String getSlackBaseURL() {
        String host = hostname;
        if(host == null || !host.endsWith(".slack.com"))
            host = ircserver;
        if(host != null && host.endsWith(".slack.com"))
            return "https://" + host;
        return null;
    }

    /*@Override
    public void save() {
        if(cid != -1)
            super.save();
    }*/
} public class User {
    //@Unique(unique = false, uniqueGroups = 1)
    public int cid;

    //@Unique(unique = false, uniqueGroups = 1)
    public int bid;

    //@Unique(unique = false, uniqueGroups = 1)
    public String nick;

    public String old_nick = null;

    public String nick_lowercase;

    public String hostmask;

    public String mode;

    public int away;

    public String away_msg;

    public String ircserver;

    public String display_name;

    public int joined;

    public long last_mention = -1;
    public long last_message = -1;

    public String getDisplayName() {
        if(display_name != null && display_name.length() > 0)
            return display_name;
        else
            return nick;
    }

    public String toString() {
        return "{cid: " + cid + ", bid: " + bid + ", nick: " + nick + ", hostmask: " + hostmask + "}";
    }
} public class AcceptListFragment extends DialogFragment {
    JsonNode acceptList;
    int cid;
    IRCCloudJSONObject event;
    AcceptListAdapter adapter;
    NetworkConnection conn;
    RecyclerView recyclerView;
    TextView empty;

    private class ViewHolder extends RecyclerView.ViewHolder {
        public RowAcceptlistBinding binding;

        public ViewHolder(View v) {
            super(v);
            binding = DataBindingUtil.bind(v);
        }
    }

    private class AcceptListAdapter extends RecyclerView.Adapter<ViewHolder> {
        @Override
        public int getItemCount() {
            return acceptList.size();
        }

        @Override
        public long getItemId(int pos) {
            return pos;
        }

        OnClickListener removeClickListener = new OnClickListener() {
            @Override
            public void onClick(View v) {
                Integer position = (Integer) v.getTag();
                try {
                    conn.say(cid, null, "/accept -" + acceptList.get(position).asText(), null);
                    conn.say(cid, null, "/accept *", null);
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    NetworkConnection.printStackTraceToCrashlytics(e);
                }
            }
        };

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = RowAcceptlistBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false).getRoot();
            return new ViewHolder(v);
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, final int position) {
            RowAcceptlistBinding row = holder.binding;
            if(acceptList.get(position).isArray())
                row.setNick(acceptList.get(position).get(0).asText());
            else
                row.setNick(acceptList.get(position).asText());
            row.removeBtn.setOnClickListener(removeClickListener);
            row.removeBtn.setColorFilter(ColorScheme.getInstance().colorControlNormal, PorterDuff.Mode.SRC_ATOP);
            row.removeBtn.setTag(position);
            row.executePendingBindings();
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Context ctx = getActivity();
        if(ctx == null)
            return null;

        LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(R.layout.recyclerview, null);
        recyclerView = v.findViewById(R.id.recycler);
        recyclerView.setLayoutManager(new LinearLayoutManager(v.getContext()));
        empty = v.findViewById(android.R.id.empty);
        empty.setText("No accepted nicks.\n\nYou can accept someone by tapping their message request or by using /accept.");
        if (savedInstanceState != null && savedInstanceState.containsKey("cid")) {
            cid = savedInstanceState.getInt("cid");
            event = new IRCCloudJSONObject(savedInstanceState.getString("event"));
            acceptList = event.getJsonNode("nicks");
            adapter = new AcceptListAdapter();
            recyclerView.setAdapter(adapter);
            if(adapter.getItemCount() > 0) {
                empty.setVisibility(View.GONE);
                recyclerView.setVisibility(View.VISIBLE);
            } else {
                empty.setVisibility(View.VISIBLE);
                recyclerView.setVisibility(View.GONE);
            }
        }
        Server s = ServersList.getInstance().getServer(cid);
        String network = s.getName();
        if (network == null || network.length() == 0)
            network = s.getHostname();
        AlertDialog d = new AlertDialog.Builder(ctx)
                .setTitle("Accept list for " + network)
                .setView(v)
                .setPositiveButton("Add Nickname", new AddClickListener())
                .setNegativeButton("Close", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                }).create();

        d.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
        return d;
    }

    class AddClickListener implements DialogInterface.OnClickListener {
        @Override
        public void onClick(DialogInterface d, int which) {
            Context ctx = getActivity();

            ServersList s = ServersList.getInstance();
            Server server = s.getServer(cid);
            AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
            LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            View view = inflater.inflate(R.layout.dialog_textprompt, null);
            TextView prompt = view.findViewById(R.id.prompt);
            final EditText input = view.findViewById(R.id.textInput);
            input.setHint("nickname");
            prompt.setText("Accept messages from this nickname");
            builder.setTitle(server.getName() + " (" + server.getHostname() + ":" + (server.getPort()) + ")");
            builder.setView(view);
            builder.setPositiveButton("Accept", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    conn.say(cid, null, "/accept " + input.getText().toString(), null);
                    conn.say(cid, null, "/accept *", null);
                    dialog.dismiss();
                }
            });
            builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.dismiss();
                }
            });
            AlertDialog dialog = builder.create();
            dialog.setOwnerActivity(getActivity());
            dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
            dialog.show();
        }
    }


    @Override
    public void onSaveInstanceState(Bundle state) {
        state.putInt("cid", cid);
        state.putString("event", event.toString());
    }

    @Override
    public void setArguments(Bundle args) {
        if (args != null) {
            cid = args.getInt("cid", 0);
            event = new IRCCloudJSONObject(args.getString("event"));
            acceptList = event.getJsonNode("nicks");
            if (getActivity() != null && cid > 0 && recyclerView != null) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (adapter == null) {
                            adapter = new AcceptListAdapter();
                            recyclerView.setAdapter(adapter);
                        } else {
                            adapter.notifyDataSetChanged();
                        }
                        if (adapter.getItemCount() > 0) {
                            empty.setVisibility(View.GONE);
                            recyclerView.setVisibility(View.VISIBLE);
                        } else {
                            empty.setVisibility(View.VISIBLE);
                            recyclerView.setVisibility(View.GONE);
                        }
                    }
                });
            }
        }
    }

    public void onResume() {
        super.onResume();
        conn = NetworkConnection.getInstance();

        if (cid > 0) {
            adapter = new AcceptListAdapter();
            recyclerView.setAdapter(adapter);
            if(adapter.getItemCount() > 0) {
                empty.setVisibility(View.GONE);
                recyclerView.setVisibility(View.VISIBLE);
            } else {
                empty.setVisibility(View.VISIBLE);
                recyclerView.setVisibility(View.GONE);
            }
        }
    }
} @SuppressLint("ValidFragment")
public class BufferOptionsFragment extends DialogFragment {
    private SwitchCompat members;
    private SwitchCompat unread;
    private SwitchCompat joinpart;
    private SwitchCompat collapse;
    private SwitchCompat notifyAll;
    private SwitchCompat autosuggest;
    private SwitchCompat readOnSelect;
    private SwitchCompat inlineFiles;
    private SwitchCompat inlineImages;
    private SwitchCompat replyCollapse;
    private SwitchCompat expandDisco;
    private SwitchCompat muted;
    private SwitchCompat formatColors;
    private int cid;
    private int bid;
    private String type;
    private String pref_type;

    public BufferOptionsFragment() {
        cid = bid = -1;
        type = null;
    }

    public BufferOptionsFragment(int cid, int bid, String type) {
        this.cid = cid;
        this.bid = bid;
        this.type = type;
        if(type.equals("channel"))
            pref_type = "channel";
        else
            pref_type = "buffer";
    }

    public JSONObject updatePref(JSONObject prefs, boolean checked, String key) throws JSONException {
        if (!checked) {
            JSONObject map;
            if (prefs.has(key))
                map = prefs.getJSONObject(key);
            else
                map = new JSONObject();
            map.put(String.valueOf(bid), true);
            prefs.put(key, map);
        } else {
            if (prefs.has(key)) {
                JSONObject map = prefs.getJSONObject(key);
                map.remove(String.valueOf(bid));
                prefs.put(key, map);
            }
        }
        return prefs;
    }

    class SaveClickListener implements DialogInterface.OnClickListener {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            JSONObject prefs = null;
            try {
                if (NetworkConnection.getInstance().getUserInfo() != null) {
                    prefs = NetworkConnection.getInstance().getUserInfo().prefs;
                    if (prefs == null) {
                        prefs = new JSONObject();
                    }

                    prefs = updatePref(prefs, unread.isChecked(), pref_type + "-disableTrackUnread");
                    prefs = updatePref(prefs, !unread.isChecked(), pref_type + "-enableTrackUnread");
                    prefs = updatePref(prefs, readOnSelect.isChecked(), pref_type + "-disableReadOnSelect");
                    prefs = updatePref(prefs, !readOnSelect.isChecked(), pref_type + "-enableReadOnSelect");
                    prefs = updatePref(prefs, !muted.isChecked(), pref_type + "-notifications-mute");
                    prefs = updatePref(prefs, muted.isChecked(), pref_type + "-notifications-mute-disable");
                    prefs = updatePref(prefs, !formatColors.isChecked(), pref_type + "-chat-color");
                    prefs = updatePref(prefs, formatColors.isChecked(), pref_type + "-chat-nocolor");
                    if (type.equals("console")) {
                        prefs = updatePref(prefs, expandDisco.isChecked(), pref_type + "-expandDisco");
                    } else {
                        if(type.equals("channel")) {
                            prefs = updatePref(prefs, members.isChecked(), pref_type + "-hiddenMembers");
                            prefs = updatePref(prefs, notifyAll.isChecked(), pref_type + "-notifications-all-disable");
                            prefs = updatePref(prefs, !notifyAll.isChecked(), pref_type + "-notifications-all");
                            prefs = updatePref(prefs, autosuggest.isChecked(), pref_type + "-disableAutoSuggest");
                        }
                        prefs = updatePref(prefs, joinpart.isChecked(), pref_type + "-hideJoinPart");
                        prefs = updatePref(prefs, !joinpart.isChecked(), pref_type + "-showJoinPart");
                        prefs = updatePref(prefs, collapse.isChecked(), pref_type + "-expandJoinPart");
                        prefs = updatePref(prefs, !collapse.isChecked(), pref_type + "-collapseJoinPart");
                        prefs = updatePref(prefs, inlineFiles.isChecked(), pref_type + "-files-disableinline");
                        prefs = updatePref(prefs, !inlineFiles.isChecked(), pref_type + "-files-enableinline");
                        prefs = updatePref(prefs, !replyCollapse.isChecked(), pref_type + "-reply-collapse");
                        prefs = updatePref(prefs, inlineImages.isChecked(), pref_type + "-inlineimages-disable");
                        prefs = updatePref(prefs, !inlineImages.isChecked(), pref_type + "-inlineimages");
                    }
                    NetworkConnection.getInstance().set_prefs(prefs.toString(), null);
                } else {
                    Toast.makeText(getActivity(), "An error occurred while saving preferences.  Please try again shortly", Toast.LENGTH_SHORT).show();
                }
            } catch (Exception e) {
                IRCCloudLog.LogException(e);
                Toast.makeText(getActivity(), "An error occurred while saving preferences.  Please try again shortly", Toast.LENGTH_SHORT).show();
            }
            dismiss();
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        try {
            notifyAll.setChecked(false);
            joinpart.setChecked(true);
            unread.setChecked(true);
            members.setChecked(true);
            collapse.setChecked(true);
            autosuggest.setChecked(true);
            readOnSelect.setChecked(false);
            inlineFiles.setChecked(true);
            inlineImages.setChecked(false);
            replyCollapse.setChecked(false);
            expandDisco.setChecked(true);
            muted.setChecked(false);
            formatColors.setChecked(true);

            if (NetworkConnection.getInstance().getUserInfo() != null) {
                JSONObject prefs = NetworkConnection.getInstance().getUserInfo().prefs;
                if (prefs != null) {
                    boolean enabled = (prefs.has("hideJoinPart") && prefs.get("hideJoinPart") instanceof Boolean && prefs.getBoolean("hideJoinPart"));
                    if (prefs.has(pref_type + "-showJoinPart")) {
                        JSONObject showMap = prefs.getJSONObject(pref_type + "-showJoinPart");
                        if (showMap.has(String.valueOf(bid)) && showMap.getBoolean(String.valueOf(bid)))
                            enabled = false;
                    }
                    if (prefs.has(pref_type + "-hideJoinPart")) {
                        JSONObject hideMap = prefs.getJSONObject(pref_type + "-hideJoinPart");
                        if (hideMap.has(String.valueOf(bid)) && hideMap.getBoolean(String.valueOf(bid)))
                            enabled = true;
                    }
                    joinpart.setChecked(!enabled);
                    enabled = !(prefs.has("disableTrackUnread") && prefs.get("disableTrackUnread") instanceof Boolean && prefs.getBoolean("disableTrackUnread"));
                    if (prefs.has(pref_type + "-disableTrackUnread")) {
                        JSONObject unreadMap = prefs.getJSONObject(pref_type + "-disableTrackUnread");
                        if (unreadMap.has(String.valueOf(bid)) && unreadMap.getBoolean(String.valueOf(bid)))
                            enabled = false;
                    }
                    if (prefs.has(pref_type + "-enableTrackUnread")) {
                        JSONObject unreadMap = prefs.getJSONObject(pref_type + "-enableTrackUnread");
                        if (unreadMap.has(String.valueOf(bid)) && unreadMap.getBoolean(String.valueOf(bid)))
                            enabled = true;
                    }
                    unread.setChecked(enabled);
                    if (prefs.has(pref_type + "-hiddenMembers")) {
                        JSONObject membersMap = prefs.getJSONObject(pref_type + "-hiddenMembers");
                        if (membersMap.has(String.valueOf(bid)) && membersMap.getBoolean(String.valueOf(bid)))
                            members.setChecked(false);
                        else
                            members.setChecked(true);
                    } else {
                        members.setChecked(true);
                    }
                    enabled = (prefs.has("expandJoinPart") && prefs.get("expandJoinPart") instanceof Boolean && prefs.getBoolean("expandJoinPart"));
                    if (prefs.has(pref_type + "-expandJoinPart")) {
                        JSONObject expandMap = prefs.getJSONObject(pref_type + "-expandJoinPart");
                        if (expandMap.has(String.valueOf(bid)) && expandMap.getBoolean(String.valueOf(bid)))
                            enabled = true;
                    }
                    if (prefs.has(pref_type + "-collapseJoinPart")) {
                        JSONObject collapseMap = prefs.getJSONObject(pref_type + "-collapseJoinPart");
                        if (collapseMap.has(String.valueOf(bid)) && collapseMap.getBoolean(String.valueOf(bid)))
                            enabled = false;
                    }
                    collapse.setChecked(!enabled);
                    enabled = (prefs.has("notifications-all") && prefs.get("notifications-all") instanceof Boolean && prefs.getBoolean("notifications-all"));
                    if (prefs.has(pref_type + "-notifications-all")) {
                        JSONObject notifyAllMap = prefs.getJSONObject(pref_type + "-notifications-all");
                        if (notifyAllMap.has(String.valueOf(bid)) && notifyAllMap.getBoolean(String.valueOf(bid)))
                            enabled = true;
                    }
                    if (prefs.has(pref_type + "-notifications-all-disable")) {
                        JSONObject notifyAllMap = prefs.getJSONObject(pref_type + "-notifications-all-disable");
                        if (notifyAllMap.has(String.valueOf(bid)) && notifyAllMap.getBoolean(String.valueOf(bid)))
                            enabled = false;
                    }
                    notifyAll.setChecked(enabled);
                    enabled = (prefs.has("notifications-mute") && prefs.get("notifications-mute") instanceof Boolean && prefs.getBoolean("notifications-mute"));
                    if (prefs.has(pref_type + "-notifications-mute")) {
                        JSONObject notifyMuteMap = prefs.getJSONObject(pref_type + "-notifications-mute");
                        if (notifyMuteMap.has(String.valueOf(bid)) && notifyMuteMap.getBoolean(String.valueOf(bid)))
                            enabled = true;
                    }
                    if (prefs.has(pref_type + "-notifications-mute-disable")) {
                        JSONObject notifyMuteMap = prefs.getJSONObject(pref_type + "-notifications-mute-disable");
                        if (notifyMuteMap.has(String.valueOf(bid)) && notifyMuteMap.getBoolean(String.valueOf(bid)))
                            enabled = false;
                    }
                    muted.setChecked(enabled);
                    if (prefs.has(pref_type + "-disableAutoSuggest")) {
                        JSONObject suggestMap = prefs.getJSONObject(pref_type + "-disableAutoSuggest");
                        if (suggestMap.has(String.valueOf(bid)) && suggestMap.getBoolean(String.valueOf(bid)))
                            autosuggest.setChecked(false);
                        else
                            autosuggest.setChecked(true);
                    } else {
                        autosuggest.setChecked(true);
                    }
                    enabled = prefs.has("enableReadOnSelect") && prefs.get("enableReadOnSelect") instanceof Boolean && prefs.getBoolean("enableReadOnSelect");
                    if (prefs.has(pref_type + "-enableReadOnSelect")) {
                        JSONObject readOnSelectMap = prefs.getJSONObject(pref_type + "-enableReadOnSelect");
                        if (readOnSelectMap.has(String.valueOf(bid)) && readOnSelectMap.getBoolean(String.valueOf(bid)))
                            enabled = true;
                    }
                    if (prefs.has(pref_type + "-disableReadOnSelect")) {
                        JSONObject readOnSelectMap = prefs.getJSONObject(pref_type + "-disableReadOnSelect");
                        if (readOnSelectMap.has(String.valueOf(bid)) && readOnSelectMap.getBoolean(String.valueOf(bid)))
                            enabled = false;
                    }
                    readOnSelect.setChecked(enabled);
                    enabled = (prefs.has("files-disableinline") && prefs.get("files-disableinline") instanceof Boolean && prefs.getBoolean("files-disableinline"));
                    if (prefs.has(pref_type + "-files-disableinline")) {
                        JSONObject noInlineMap = prefs.getJSONObject(pref_type + "-files-disableinline");
                        if (noInlineMap.has(String.valueOf(bid)) && noInlineMap.getBoolean(String.valueOf(bid)))
                            enabled = true;
                    }
                    if (prefs.has(pref_type + "-files-enableinline")) {
                        JSONObject inlineMap = prefs.getJSONObject(pref_type + "-files-enableinline");
                        if (inlineMap.has(String.valueOf(bid)) && inlineMap.getBoolean(String.valueOf(bid)))
                            enabled = false;
                    }
                    inlineFiles.setChecked(!enabled);
                    if(prefs.has("inlineimages") && prefs.get("inlineimages") instanceof Boolean && prefs.getBoolean("inlineimages")) {
                        JSONObject inlineImagesMap = null;
                        if (prefs.has(pref_type + "-inlineimages-disable"))
                            inlineImagesMap = prefs.getJSONObject(pref_type + "-inlineimages-disable");

                        inlineImages.setChecked(!(inlineImagesMap != null && inlineImagesMap.has(String.valueOf(bid)) && inlineImagesMap.getBoolean(String.valueOf(bid))));
                    } else {
                        JSONObject inlineImagesMap = null;
                        if (prefs.has(pref_type + "-inlineimages"))
                            inlineImagesMap = prefs.getJSONObject(pref_type + "-inlineimages");

                        inlineImages.setChecked((inlineImagesMap != null && inlineImagesMap.has(String.valueOf(bid)) && inlineImagesMap.getBoolean(String.valueOf(bid))));
                    }
                    if (prefs.has(pref_type + "-reply-collapse")) {
                        JSONObject collapseMap = prefs.getJSONObject(pref_type + "-reply-collapse");
                        if (collapseMap.has(String.valueOf(bid)) && collapseMap.getBoolean(String.valueOf(bid)))
                            replyCollapse.setChecked(true);
                        else
                            replyCollapse.setChecked(false);
                    } else {
                        replyCollapse.setChecked(false);
                    }
                    if (prefs.has(pref_type + "-expandDisco")) {
                        JSONObject expandMap = prefs.getJSONObject(pref_type + "-expandDisco");
                        if (expandMap.has(String.valueOf(bid)) && expandMap.getBoolean(String.valueOf(bid)))
                            expandDisco.setChecked(false);
                        else
                            expandDisco.setChecked(true);
                    }
                    enabled = !(prefs.has("chat-nocolor") && prefs.get("chat-nocolor") instanceof Boolean && prefs.getBoolean("chat-nocolor"));
                    if (prefs.has(pref_type + "-chat-color")) {
                        JSONObject chatColorMap = prefs.getJSONObject(pref_type + "-chat-color");
                        if (chatColorMap.has(String.valueOf(bid)) && chatColorMap.getBoolean(String.valueOf(bid)))
                            enabled = true;
                    }
                    if (prefs.has(pref_type + "-chat-nocolor")) {
                        JSONObject chatNoColorMap = prefs.getJSONObject(pref_type + "-chat-nocolor");
                        if (chatNoColorMap.has(String.valueOf(bid)) && chatNoColorMap.getBoolean(String.valueOf(bid)))
                            enabled = false;
                    }
                    formatColors.setChecked(enabled);
                }
            }
            if (!getActivity().getResources().getBoolean(R.bool.isTablet))
                members.setVisibility(View.GONE);
        } catch (JSONException e) {
            NetworkConnection.printStackTraceToCrashlytics(e);
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Context ctx = getActivity();
        if(ctx == null)
            return null;
        LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(R.layout.dialog_buffer_options, null);
        expandDisco = v.findViewById(R.id.expandDisco);
        members = v.findViewById(R.id.members);
        unread = v.findViewById(R.id.unread);
        notifyAll = v.findViewById(R.id.notifyAll);
        muted = v.findViewById(R.id.muted);
        joinpart = v.findViewById(R.id.joinpart);
        collapse = v.findViewById(R.id.collapse);
        replyCollapse = v.findViewById(R.id.replyCollapse);
        autosuggest = v.findViewById(R.id.autosuggest);
        readOnSelect = v.findViewById(R.id.readOnSelect);
        inlineFiles = v.findViewById(R.id.inlineFiles);
        formatColors = v.findViewById(R.id.formatColors);
        inlineImages = v.findViewById(R.id.inlineImages);
        inlineImages.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(inlineImages.isChecked()) {
                    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
                    builder.setTitle("Warning");
                    builder.setMessage("External URLs may load insecurely and could result in your IP address being revealed to external site operators");

                    builder.setPositiveButton("Enable", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.dismiss();
                        }
                    });
                    builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            inlineImages.setChecked(false);
                            dialog.dismiss();
                        }
                    });
                    AlertDialog d = builder.create();
                    if (!getActivity().isFinishing()) {
                        d.setOwnerActivity(getActivity());
                        d.show();
                    }
                }
            }
        });

        if (savedInstanceState != null && bid == -1 && savedInstanceState.containsKey("bid")) {
            bid = savedInstanceState.getInt("bid");
            cid = savedInstanceState.getInt("cid");
            type = savedInstanceState.getString("type");
            if(type.equals("channel"))
                pref_type = "channel";
            else
                pref_type = "buffer";
        }

        if (type != null && type.equals("console")) {
            notifyAll.setVisibility(View.GONE);
            members.setVisibility(View.GONE);
            autosuggest.setVisibility(View.GONE);
            joinpart.setVisibility(View.GONE);
            collapse.setVisibility(View.GONE);
            inlineFiles.setVisibility(View.GONE);
            inlineImages.setVisibility(View.GONE);
            replyCollapse.setVisibility(View.GONE);
        } else {
            expandDisco.setVisibility(View.GONE);
            if(type.equals("conversation")) {
                notifyAll.setVisibility(View.GONE);
                members.setVisibility(View.GONE);
                autosuggest.setVisibility(View.GONE);
            }
        }

        if(BuildConfig.ENTERPRISE)
            muted.setVisibility(View.GONE);

        return new AlertDialog.Builder(ctx)
                .setTitle("Display Options")
                .setView(v)
                .setPositiveButton("Save", new SaveClickListener())
                .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                })
                .create();
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        outState.putInt("bid", bid);
        outState.putInt("cid", cid);
        outState.putString("type", type);
    }
} public class BuffersListFragment extends Fragment implements NetworkConnection.IRCEventHandler {
    private RecyclerView recyclerView;
    private LinearLayoutManager layoutManager;
    private NetworkConnection conn;
    private BufferListAdapter adapter;
    private OnBufferSelectedListener mListener;
    private LinearLayout topUnreadIndicator = null;
    private LinearLayout topUnreadIndicatorColor = null;
    private LinearLayout topUnreadIndicatorBorder = null;
    private LinearLayout bottomUnreadIndicator = null;
    private LinearLayout bottomUnreadIndicatorColor = null;
    private LinearLayout bottomUnreadIndicatorBorder = null;
    private ProgressBar progressBar = null;
    private RefreshTask refreshTask = null;
    private boolean ready = false;
    private boolean requestingArchives = false;
    public boolean readOnly = false;

    private int firstUnreadPosition = -1;
    private int lastUnreadPosition = -1;
    private int firstHighlightPosition = -1;
    private int lastHighlightPosition = -1;
    private int firstFailurePosition = -1;
    private int lastFailurePosition = -1;

    private SparseBooleanArray mExpandArchives = new SparseBooleanArray();
    private SparseBooleanArray mExpandCids = new SparseBooleanArray();

    private int selected_bid = -1;

    public static class ViewHolder extends RecyclerView.ViewHolder {
        public RowBufferBinding binding;

        public ViewHolder(View v) {
            super(v);
            binding = DataBindingUtil.bind(v);
        }
    }

    private class BufferListAdapter extends RecyclerView.Adapter<ViewHolder> {
        ArrayList<Buffer> data;
        HashMap<String,Integer> nameCounts;

        public int positionForBid(int bid) {
            for (int i = 0; i < data.size(); i++) {
                Buffer e = data.get(i);
                if (e.getBid() == bid)
                    return i;
            }
            return -1;
        }

        public BufferListAdapter() {
            data = new ArrayList<>(BuffersList.getInstance().count() + ServersList.getInstance().count() + 10);
            nameCounts = new HashMap<>();
        }

        public void setItems(ArrayList<Buffer> items) {
            data = items;
            nameCounts = new HashMap<>();

            try {
                if (NetworkConnection.getInstance().getUserInfo() != null) {
                    JSONObject prefs = NetworkConnection.getInstance().getUserInfo().prefs;
                    HashSet<Integer> pinned = new HashSet<>();
                    if (prefs.has("pinnedBuffers")) {
                        JSONArray pinnedBuffers = prefs.getJSONArray("pinnedBuffers");
                        if (pinnedBuffers.length() > 0) {
                            for (int i = 0; i < pinnedBuffers.length(); i++) {
                                pinned.add(pinnedBuffers.getInt(i));
                            }

                            for (Buffer b : data) {
                                if (pinned.contains(b.getBid())) {
                                    int count = 1;
                                    if (nameCounts.containsKey(b.getName()))
                                        count += nameCounts.get(b.getName());
                                    nameCounts.put(b.getName(), count);
                                }
                            }
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        public synchronized void updateCollapsed(Server s) {
            ArrayList<Buffer> buffers = BuffersList.getInstance().getBuffersForServer(s.getCid());
            int collapsed_unread = 0;
            int collapsed_highlights = 0;
            for (Buffer b : buffers) {
                if (b.getArchived() == 0 || b.isConsole()) {
                    collapsed_unread += b.getUnread();
                    collapsed_highlights += b.getHighlights();
                }
            }

            s.collapsed.setUnread(collapsed_unread);
            s.collapsed.setHighlights(collapsed_highlights);

            int pos = 0;
            for(int i = 0; i < data.size(); i++) {
                Buffer b = data.get(i);
                if(b.isCollapsed() && b.getCid() == s.getCid()) {
                    pos = i;
                    if(getActivity() != null) {
                        final int p = pos;
                        getActivity().runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                notifyItemChanged(p);
                            }
                        });
                    }
                    break;
                }
            }

            if(pos > 0) {
                if(collapsed_unread > 0) {
                    if (firstUnreadPosition == -1 || firstUnreadPosition > pos)
                        firstUnreadPosition = pos;
                    if (lastUnreadPosition == -1 || lastUnreadPosition < pos)
                        lastUnreadPosition = pos;
                } else {
                    if (firstUnreadPosition == pos) {
                        firstUnreadPosition = -1;
                        for (int i = 0; i < data.size(); i++) {
                            if (data.get(i).getUnread() > 0) {
                                firstUnreadPosition = i;
                                break;
                            }
                        }
                    }
                    if (lastUnreadPosition == pos) {
                        lastUnreadPosition = -1;
                        for (int i = pos; i >= 0; i--) {
                            if (data.get(i).getUnread() > 0) {
                                lastUnreadPosition = i;
                                break;
                            }
                        }
                    }
                }

                if (collapsed_highlights > 0) {
                    if (firstHighlightPosition == -1 || firstHighlightPosition > pos)
                        firstHighlightPosition = pos;
                    if (lastHighlightPosition == -1 || lastHighlightPosition < pos)
                        lastHighlightPosition = pos;
                } else {
                    if (firstHighlightPosition == pos) {
                        firstHighlightPosition = -1;
                        for (int i = 0; i < data.size(); i++) {
                            if (data.get(i).getHighlights() > 0) {
                                firstHighlightPosition = i;
                                break;
                            }
                        }
                    }
                    if (lastHighlightPosition == pos) {
                        lastHighlightPosition = -1;
                        for (int i = pos; i >= 0; i--) {
                            if (data.get(i).getHighlights() > 0) {
                                lastHighlightPosition = i;
                                break;
                            }
                        }
                    }
                }
            }
        }

        public synchronized void updateBuffer(Buffer b) {
            final int pos = positionForBid(b.getBid());
            if (pos >= 0 && data != null && pos < data.size()) {
                Buffer e = data.get(pos);
                try {
                    JSONObject prefs = NetworkConnection.getInstance().getUserInfo().prefs;
                    String pref_type = b.isChannel() ? "channel" : "buffer";
                    boolean muted = (prefs.has("notifications-mute") && prefs.get("notifications-mute") instanceof Boolean && prefs.getBoolean("notifications-mute"));
                    if (prefs.has(pref_type + "-notifications-mute")) {
                        JSONObject notifyMuteMap = prefs.getJSONObject(pref_type + "-notifications-mute");
                        if (notifyMuteMap.has(String.valueOf(b.getBid())) && notifyMuteMap.getBoolean(String.valueOf(b.getBid())))
                            muted = true;
                    }
                    if (prefs.has(pref_type + "-notifications-mute-disable")) {
                        JSONObject notifyMuteMap = prefs.getJSONObject(pref_type + "-notifications-mute-disable");
                        if (notifyMuteMap.has(String.valueOf(b.getBid())) && notifyMuteMap.getBoolean(String.valueOf(b.getBid())))
                            muted = false;
                    }
                    b.setMuted(muted);
                } catch (Exception e1) {

                }
                if(getActivity() != null) {
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            notifyItemChanged(pos);
                        }
                    });
                }

                if (e.getUnread() > 0) {
                    if (firstUnreadPosition == -1 || firstUnreadPosition > pos)
                        firstUnreadPosition = pos;
                    if (lastUnreadPosition == -1 || lastUnreadPosition < pos)
                        lastUnreadPosition = pos;
                } else {
                    if (firstUnreadPosition == pos) {
                        firstUnreadPosition = -1;
                        for (int i = 0; i < data.size(); i++) {
                            if (data.get(i).getUnread() > 0) {
                                firstUnreadPosition = i;
                                break;
                            }
                        }
                    }
                    if (lastUnreadPosition == pos) {
                        lastUnreadPosition = -1;
                        for (int i = pos; i >= 0; i--) {
                            if (data.get(i).getUnread() > 0) {
                                lastUnreadPosition = i;
                                break;
                            }
                        }
                    }
                }

                if (e.getHighlights() > 0) {
                    if (firstHighlightPosition == -1 || firstHighlightPosition > pos)
                        firstHighlightPosition = pos;
                    if (lastHighlightPosition == -1 || lastHighlightPosition < pos)
                        lastHighlightPosition = pos;
                } else {
                    if (firstHighlightPosition == pos) {
                        firstHighlightPosition = -1;
                        for (int i = 0; i < data.size(); i++) {
                            if (data.get(i).getHighlights() > 0) {
                                firstHighlightPosition = i;
                                break;
                            }
                        }
                    }
                    if (lastHighlightPosition == pos) {
                        lastHighlightPosition = -1;
                        for (int i = pos; i >= 0; i--) {
                            if (data.get(i).getHighlights() > 0) {
                                lastHighlightPosition = i;
                                break;
                            }
                        }
                    }
                }

                if (e.isConsole()) {
                    Server s = ServersList.getInstance().getServer(e.getCid());
                    if (s != null) {
                        ObjectNode fail_info = s.getFail_info();

                        if (fail_info != null && fail_info.has("type")) {
                            if (firstFailurePosition == -1 || firstFailurePosition > pos)
                                firstFailurePosition = pos;
                            if (lastFailurePosition == -1 || lastFailurePosition < pos)
                                lastFailurePosition = pos;
                        } else {
                            if (firstFailurePosition == pos) {
                                firstFailurePosition = -1;
                                for (int i = 0; i < data.size(); i++) {
                                    Buffer j = data.get(i);
                                    s = j.getServer();
                                    if (j.isConsole() && s != null) {
                                        fail_info = s.getFail_info();
                                        if (fail_info != null && fail_info.has("type")) {
                                            firstFailurePosition = i;
                                            break;
                                        }
                                    }
                                }
                            }
                            if (lastFailurePosition == pos) {
                                lastFailurePosition = -1;
                                for (int i = pos; i >= 0; i--) {
                                    Buffer j = data.get(i);
                                    s = j.getServer();
                                    if (j.isConsole() && s != null) {
                                        fail_info = s.getFail_info();
                                        if (fail_info != null && fail_info.has("type")) {
                                            lastFailurePosition = i;
                                            break;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                if (getActivity() != null) {
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            if (layoutManager != null)
                                updateUnreadIndicators(layoutManager.findFirstVisibleItemPosition(), layoutManager.findLastVisibleItemPosition());
                        }
                    });
                }
            } else {
                if (getActivity() != null) {
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            refresh();
                        }
                    });
                }
            }
        }

        int unreadPositionAbove(int pos) {
            if (pos > 0) {
                for (int i = pos - 1; i >= 0; i--) {
                    Buffer e = data.get(i);
                    if (e.getUnread() > 0 || e.getHighlights() > 0 || (e.isConsole() && e.getServer().isFailed()))
                        return i;
                }
            }
            return 0;
        }

        int unreadPositionBelow(int pos) {
            if (pos >= 0) {
                for (int i = pos; i < data.size(); i++) {
                    Buffer e = data.get(i);
                    if (e.getUnread() > 0 || e.getHighlights() > 0 || (e.isConsole() && e.getServer().isFailed()))
                        return i;
                }
            }
            return data.size() - 1;
        }

        @Override
        public int getItemCount() {
            return data.size();
        }

        public Object getItem(int position) {
            if (position < data.size())
                return data.get(position);
            else
                return null;
        }

        @Override
        public long getItemId(int position) {
            if (position < data.size()) {
                Buffer e = data.get(position);
                return e.getBid();
            } else {
                return -1;
            }
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = RowBufferBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false).getRoot();
            return new ViewHolder(v);
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, final int position) {
            final Buffer b = data.get(position);
            int count = 0;
            if(nameCounts.containsKey(b.getName()))
                count = nameCounts.get(b.getName());
            b.showServerSuffix(count > 1);
            RowBufferBinding row = holder.binding;

            row.setBuffer(b);
            row.setServer(b.getServer());
            row.setSelected(selected_bid);
            row.setReadOnly(readOnly);
            if(b.getType().equals(Buffer.TYPE_ARCHIVES_HEADER))
                row.setShowSpinner(requestingArchives && b.getArchived() > 0 && b.getServer() != null && b.getServer().deferred_archives > 0);
            else
                row.setShowSpinner(!readOnly && b.getShowSpinner());

            row.getRoot().setContentDescription(b.getContentDescription());

            row.getRoot().setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View view) {
                    switch (b.getType()) {
                        case Buffer.TYPE_ARCHIVES_HEADER:
                            mExpandArchives.put(b.getCid(), !mExpandArchives.get(b.getCid(), false));
                            if(mExpandArchives.get(b.getCid(), false) && b.getServer() != null && b.getServer().deferred_archives > 0) {
                                requestingArchives = true;
                                NetworkConnection.getInstance().request_archives(b.getCid());
                            }
                            refresh();
                            return;
                        case Buffer.TYPE_JOIN_CHANNEL:
                            AddChannelFragment newFragment = new AddChannelFragment();
                            newFragment.setDefaultCid(b.getCid());
                            newFragment.show(getActivity().getSupportFragmentManager(), "dialog");
                            mListener.addButtonPressed(b.getCid());
                            return;
                        case Buffer.TYPE_SPAM:
                            SpamFragment spamFragment = new SpamFragment();
                            spamFragment.setCid(b.getCid());
                            spamFragment.show(getActivity().getSupportFragmentManager(), "spam");
                            return;
                        case Buffer.TYPE_COLLAPSED:
                            if(b.getArchived() > 0) {
                                mExpandCids.put(b.getCid(), true);
                                NetworkConnection.getInstance().unarchiveBuffer(b.getCid(), b.getBid(), null);
                            } else {
                                mExpandCids.delete(b.getCid());
                                NetworkConnection.getInstance().archiveBuffer(b.getCid(), b.getBid(), null);
                            }
                            return;
                        case Buffer.TYPE_PINNED:
                            return;
                        case Buffer.TYPE_ADD_NETWORK:
                            mListener.addNetwork();
                            return;
                    }
                    mListener.onBufferSelected(b.getBid());
                }
            });

            row.getRoot().setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View view) {
                    return (b.isChannel() || b.isConversation() || b.isConsole()) && mListener.onBufferLongClicked(b);
                }
            });

            row.executePendingBindings();
        }
    }

    private class RefreshTask extends AsyncTaskEx<Void, Void, Void> {
        private ArrayList<Buffer> entries = new ArrayList<>();
        private boolean shouldScroll = false;

        private void add(int position, Buffer b, Server s) {
            entries.add(position,b);
            if (b.getUnread() > 0 && firstUnreadPosition == -1)
                firstUnreadPosition = position;
            if (b.getUnread() > 0 && (lastUnreadPosition == -1 || lastUnreadPosition < position))
                lastUnreadPosition = position;
            if (b.getHighlights() > 0 && firstHighlightPosition == -1)
                firstHighlightPosition = position;
            if (b.getHighlights() > 0 && (lastHighlightPosition == -1 || lastHighlightPosition < position))
                lastHighlightPosition = position;
            if(s != null) {
                if (b.isConsole() && s.isFailed() && firstFailurePosition == -1)
                    firstFailurePosition = position;
                if (b.isConsole() && s.isFailed() && (lastFailurePosition == -1 || lastFailurePosition < position))
                    lastFailurePosition = position;
            }
        }

        @Override
        protected synchronized Void doInBackground(Void... params) {
            if (!NetworkConnection.getInstance().ready || !ready || isCancelled() || NetworkConnection.getInstance().getUserInfo() == null) {
                IRCCloudLog.Log(Log.WARN, "IRCCloud", "BuffersListFragment not ready or cancelled " + ready + " " + isCancelled());
                return null;
            }

            JSONObject prefs = NetworkConnection.getInstance().getUserInfo().prefs;
            SparseArray<Server> serversArray = ServersList.getInstance().getServers();
            ArrayList<Server> servers = new ArrayList<>();

            for (int i = 0; i < serversArray.size(); i++) {
                Server s = serversArray.valueAt(i);
                if(s != null)
                    servers.add(s);
            }
            Collections.sort(servers);
            if (adapter == null) {
                IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "Created new BufferListAdapter");
                adapter = new BufferListAdapter();
            }

            if(adapter.getItemCount() == 0)
                shouldScroll = true;

            firstUnreadPosition = -1;
            lastUnreadPosition = -1;
            firstHighlightPosition = -1;
            lastHighlightPosition = -1;
            firstFailurePosition = -1;
            lastFailurePosition = -1;
            int position = 0;

            HashSet<Integer> pinned = new HashSet<>();
            try {
                if (prefs.has("pinnedBuffers")) {
                    JSONArray pinnedBuffers = prefs.getJSONArray("pinnedBuffers");
                    if(pinnedBuffers.length() > 0) {
                        for (int i = 0; i < pinnedBuffers.length(); i++) {
                            Buffer b = BuffersList.getInstance().getBuffer(pinnedBuffers.getInt(i));
                            if (b != null && b.getArchived() == 0) {
                                if(pinned.size() == 0) {
                                    Buffer heading = new Buffer();
                                    heading.setName("Pinned");
                                    heading.setType(Buffer.TYPE_PINNED);
                                    add(position,heading,null);
                                    position++;
                                }
                                pinned.add(b.getBid());
                                add(position,b,b.getServer());
                                position++;
                            }
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

            for (Server s : servers) {
                if (isCancelled())
                    return null;

                int spamCount = 0;
                int archiveCount = s.deferred_archives;
                Buffer collapsed = null;
                int collapsed_unread = 0;
                int collapsed_highlights = 0;
                int collapsed_row = 0;
                ArrayList<Buffer> buffers = BuffersList.getInstance().getBuffersForServer(s.getCid());
                for (Buffer b : buffers) {
                    if (isCancelled())
                        return null;

                    if(pinned.contains(b.getBid()))
                        continue;

                    try {
                        String pref_type = b.isChannel() ? "channel" : "buffer";
                        boolean muted = (prefs.has("notifications-mute") && prefs.get("notifications-mute") instanceof Boolean && prefs.getBoolean("notifications-mute"));
                        if (prefs.has(pref_type + "-notifications-mute")) {
                            JSONObject notifyMuteMap = prefs.getJSONObject(pref_type + "-notifications-mute");
                            if (notifyMuteMap.has(String.valueOf(b.getBid())) && notifyMuteMap.getBoolean(String.valueOf(b.getBid())))
                                muted = true;
                        }
                        if (prefs.has(pref_type + "-notifications-mute-disable")) {
                            JSONObject notifyMuteMap = prefs.getJSONObject(pref_type + "-notifications-mute-disable");
                            if (notifyMuteMap.has(String.valueOf(b.getBid())) && notifyMuteMap.getBoolean(String.valueOf(b.getBid())))
                                muted = false;
                        }
                        b.setMuted(muted);
                    } catch (Exception e) {

                    }

                    if (b.getArchived() == 0 || b.isConsole()) {
                        if(collapsed != null && !mExpandCids.get(b.getCid(), false) && b.getBid() != selected_bid) {
                            collapsed_unread += b.getUnread();
                            collapsed_highlights += b.getHighlights();
                        } else {
                            add(position,b,s);
                            if (!readOnly && b.isConversation() && b.getUnread() > 0 && EventsList.getInstance().getSizeOfBuffer(b.getBid()) == 1)
                                spamCount++;
                            position++;
                        }

                        if(!BuildConfig.ENTERPRISE) {
                            if (b.isConsole() && (b.getArchived() == 1 || mExpandCids.get(s.getCid(), false))) {
                                collapsed = new Buffer();
                                collapsed.setCid(s.getCid());
                                collapsed.setBid(b.getBid());
                                collapsed.setName(mExpandCids.get(s.getCid(), false) ? "Collapse" : "Expand");
                                collapsed.setArchived(b.getArchived());
                                collapsed.setType(Buffer.TYPE_COLLAPSED);
                                s.collapsed = collapsed;
                                collapsed_row = position;
                            }
                        }
                    } else if(!b.isConsole()) {
                        archiveCount++;
                    }
                }
                if(collapsed != null) {
                    collapsed.setUnread(collapsed_unread);
                    collapsed.setHighlights(collapsed_highlights);
                    add(collapsed_row, collapsed, s);
                    position++;
                }
                if (collapsed == null || mExpandCids.get(s.getCid(), false)) {
                    if (spamCount > 3) {
                        Buffer spam = new Buffer();
                        spam.setCid(s.getCid());
                        spam.setName("Spam detected");
                        spam.setType(Buffer.TYPE_SPAM);
                        for (int i = 0; i < entries.size(); i++) {
                            Buffer b = entries.get(i);
                            if (b.getCid() == spam.getCid() && b.isConversation()) {
                                entries.add(i, spam);
                                break;
                            }
                        }
                        position++;
                    }
                    if (archiveCount > 0) {
                        Buffer header = new Buffer();
                        header.setCid(s.getCid());
                        header.setName("Archives");
                        header.setType(Buffer.TYPE_ARCHIVES_HEADER);
                        header.setArchived(mExpandArchives.get(s.getCid(), false) ? 1 : 0);
                        entries.add(header);
                        position++;
                        if (mExpandArchives.get(s.getCid(), false)) {
                            for (Buffer b : buffers) {
                                if (b.getArchived() == 1) {
                                    entries.add(b);
                                    position++;
                                }
                            }
                        }
                    }
                    if (buffers.size() == 1 && s.getStatus().equals("connected_ready") && !readOnly && archiveCount == 0) {
                        Buffer join = new Buffer();
                        join.setCid(s.getCid());
                        join.setName("Join a channel");
                        join.setType(Buffer.TYPE_JOIN_CHANNEL);
                        entries.add(join);
                    }
                }
            }

            if(!readOnly) {
                Buffer add_network = new Buffer();
                add_network.setName("Add a network");
                add_network.setType(Buffer.TYPE_ADD_NETWORK);
                entries.add(add_network);
            }

            IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "Buffers list adapter contains " + entries.size() + " entries");
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            if (isCancelled()) {
                IRCCloudLog.Log(Log.WARN, "IRCCloud", "BuffersListFragment: OnPostExecute: This refresh task was cancelled");
                return;
            }

            refreshTask = null;

            if (adapter == null)
                return;

            adapter.setItems(entries);

            if (recyclerView.getAdapter() == null && entries.size() > 0) {
                recyclerView.setAdapter(adapter);
            } else
                adapter.notifyDataSetChanged();

            if (layoutManager != null)
                updateUnreadIndicators(layoutManager.findFirstVisibleItemPosition(), layoutManager.findLastVisibleItemPosition());
            else {//The activity view isn't ready yet, try again
                IRCCloudLog.Log(Log.WARN, "IRCCloud", "BuffersListFragment: OnPostExecute: The activity isn't ready yet, will retry");
                refreshTask = new RefreshTask();
                refreshTask.execute((Void) null);
            }
            progressBar.setVisibility(View.GONE);
            if(shouldScroll)
                setSelectedBid(selected_bid);
        }
    }

    public void setSelectedBid(final int bid) {
        int last_bid = selected_bid;
        selected_bid = bid;
        if (adapter != null) {
            Buffer b = BuffersList.getInstance().getBuffer(last_bid);
            if (b != null)
                adapter.updateBuffer(b);
            b = BuffersList.getInstance().getBuffer(bid);
            if (b != null)
                adapter.updateBuffer(b);

            recyclerView.post(new Runnable() {
                @Override
                public void run() {
                    int pos = adapter.positionForBid(bid);
                    if(pos < layoutManager.findFirstCompletelyVisibleItemPosition() || pos > layoutManager.findLastCompletelyVisibleItemPosition()) {
                        int center = (layoutManager.findLastCompletelyVisibleItemPosition() - layoutManager.findFirstCompletelyVisibleItemPosition())/2;
                        if(pos+center < adapter.getItemCount())
                            pos += center;
                        else
                            pos = adapter.getItemCount() - 1;
                        recyclerView.scrollToPosition(pos);
                    }
                }
            });
        } else {
            IRCCloudLog.Log(Log.WARN, "IRCCloud", "BufferListFragment: Request to set BID but I don't have an adapter yet, refreshing");
            refreshTask = new RefreshTask();
            refreshTask.doInBackground((Void) null);
            if(!refreshTask.isCancelled())
                refreshTask.onPostExecute(null);
            IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "Done");
        }
    }

    private void updateUnreadIndicators(int first, int last) {
        if (readOnly) {
            if (topUnreadIndicator != null)
                topUnreadIndicator.setVisibility(View.GONE);
            if (bottomUnreadIndicator != null)
                bottomUnreadIndicator.setVisibility(View.GONE);
        } else {
            if (topUnreadIndicator != null) {
                if (firstFailurePosition != -1 && first > firstFailurePosition) {
                    topUnreadIndicator.setVisibility(View.VISIBLE);
                    topUnreadIndicatorColor.setBackgroundResource(R.drawable.network_fail_bg);
                    topUnreadIndicatorBorder.setBackgroundResource(R.drawable.networkErrorBorder);
                } else {
                    topUnreadIndicator.setVisibility(View.GONE);
                }
                if (firstUnreadPosition != -1 && first > firstUnreadPosition) {
                    topUnreadIndicator.setVisibility(View.VISIBLE);
                    topUnreadIndicatorColor.setBackgroundResource(R.drawable.row_unread_border);
                    topUnreadIndicatorBorder.setBackgroundResource(R.drawable.unreadBorder);
                }
                if ((lastHighlightPosition != -1 && first > lastHighlightPosition) ||
                        (firstHighlightPosition != -1 && first > firstHighlightPosition)) {
                    topUnreadIndicator.setVisibility(View.VISIBLE);
                    topUnreadIndicatorColor.setBackgroundResource(R.drawable.highlight_red);
                    topUnreadIndicatorBorder.setBackgroundResource(R.drawable.highlightBorder);
                }
            }
            if (bottomUnreadIndicator != null) {
                if (lastFailurePosition != -1 && last < lastFailurePosition) {
                    bottomUnreadIndicator.setVisibility(View.VISIBLE);
                    bottomUnreadIndicatorColor.setBackgroundResource(R.drawable.network_fail_bg);
                    bottomUnreadIndicatorBorder.setBackgroundResource(R.drawable.networkErrorBorder);
                } else {
                    bottomUnreadIndicator.setVisibility(View.GONE);
                }
                if (lastUnreadPosition != -1 && last < lastUnreadPosition) {
                    bottomUnreadIndicator.setVisibility(View.VISIBLE);
                    bottomUnreadIndicatorColor.setBackgroundResource(R.drawable.row_unread_border);
                    bottomUnreadIndicatorBorder.setBackgroundResource(R.drawable.unreadBorder);
                }
                if ((firstHighlightPosition != -1 && last < firstHighlightPosition) ||
                        (lastHighlightPosition != -1 && last < lastHighlightPosition)) {
                    bottomUnreadIndicator.setVisibility(View.VISIBLE);
                    bottomUnreadIndicatorColor.setBackgroundResource(R.drawable.highlight_red);
                    bottomUnreadIndicatorBorder.setBackgroundResource(R.drawable.highlightBorder);
                }
            }
        }
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        conn = NetworkConnection.getInstance();
        conn.addHandler(this);
    }

    @SuppressWarnings("unchecked")
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.bufferslist, null);
        progressBar = view.findViewById(R.id.bufferprogress);
        recyclerView = view.findViewById(android.R.id.list);
        layoutManager = new LinearLayoutManager(view.getContext());
        recyclerView.setLayoutManager(layoutManager);
        recyclerView.setItemAnimator(null);
        topUnreadIndicator = view.findViewById(R.id.topUnreadIndicator);
        topUnreadIndicator.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                if(adapter != null) {
                    try {
                        int scrollTo = adapter.unreadPositionAbove(layoutManager.findFirstVisibleItemPosition()) - 1;
                        if (scrollTo > 0)
                            recyclerView.smoothScrollToPosition(scrollTo);
                        else
                            recyclerView.smoothScrollToPosition(0);
                    } catch (Exception e) {
                    }
                    updateUnreadIndicators(layoutManager.findFirstVisibleItemPosition(), layoutManager.findLastVisibleItemPosition());
                }
            }

        });
        topUnreadIndicatorColor = view.findViewById(R.id.topUnreadIndicatorColor);
        topUnreadIndicatorBorder = view.findViewById(R.id.topUnreadIndicatorBorder);
        bottomUnreadIndicator = view.findViewById(R.id.bottomUnreadIndicator);
        bottomUnreadIndicator.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                if(adapter != null) {
                    try {
                        int scrollTo = adapter.unreadPositionBelow(layoutManager.findLastVisibleItemPosition()) + 1;
                        if (scrollTo < adapter.getItemCount())
                            recyclerView.smoothScrollToPosition(scrollTo);
                        else
                            recyclerView.smoothScrollToPosition(adapter.getItemCount() - 1);
                    } catch (Exception e) {
                    }
                    updateUnreadIndicators(layoutManager.findFirstVisibleItemPosition(), layoutManager.findLastVisibleItemPosition());
                }
            }

        });
        bottomUnreadIndicatorColor = view.findViewById(R.id.bottomUnreadIndicatorColor);
        bottomUnreadIndicatorBorder = view.findViewById(R.id.bottomUnreadIndicatorBorder);
        recyclerView.addOnScrollListener(new OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                updateUnreadIndicators(layoutManager.findFirstVisibleItemPosition(), layoutManager.findLastVisibleItemPosition());
            }

            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            }
        });

        ready = NetworkConnection.getInstance().ready;

        if (ready) {
            if (savedInstanceState != null && savedInstanceState.containsKey("expandedArchives")) {
                ArrayList<Integer> expandedArchives = savedInstanceState.getIntegerArrayList("expandedArchives");
                Iterator<Integer> i = expandedArchives.iterator();
                while (i.hasNext()) {
                    Integer cid = i.next();
                    mExpandArchives.put(cid, true);
                }
            }
            refreshTask = new RefreshTask();
            refreshTask.doInBackground((Void) null);
            if(!refreshTask.isCancelled())
                refreshTask.onPostExecute(null);
            if (savedInstanceState != null && savedInstanceState.containsKey("scrollPosition"))
                recyclerView.scrollToPosition(savedInstanceState.getInt("scrollPosition"));
        }
        return view;
    }

    @Override
    public void onSaveInstanceState(Bundle state) {
        if (adapter != null && adapter.data != null && adapter.data.size() > 0) {
            ArrayList<Integer> expandedArchives = new ArrayList<>();
            SparseArray<Server> servers = ServersList.getInstance().getServers();
            for (int i = 0; i < servers.size(); i++) {
                Server s = servers.valueAt(i);
                if (s != null && mExpandArchives.get(s.getCid(), false))
                    expandedArchives.add(s.getCid());
            }
            state.putIntegerArrayList("expandedArchives", expandedArchives);
            if (recyclerView != null)
                state.putInt("scrollPosition", layoutManager.findFirstVisibleItemPosition());
        }
    }

    public void onResume() {
        super.onResume();
        ready = conn.ready;
        refresh();
    }

    @Override public void onDestroy() {
        super.onDestroy();
        if (conn != null)
            conn.removeHandler(this);
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mListener = (OnBufferSelectedListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString() + " must implement OnBufferSelectedListener");
        }
    }

    public void onIRCEvent(int what, Object obj) {
        Buffer b;
        IRCCloudJSONObject object;
        Event event;
        switch (what) {
            case NetworkConnection.EVENT_BUFFERARCHIVED:
                b = BuffersList.getInstance().getBuffer((int)obj);
                if(b != null && b.isConsole())
                    mExpandCids.delete(b.getCid());
                if(getActivity() != null)
                    getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        refresh();
                    }
                });
                break;
            case NetworkConnection.EVENT_CHANNELMODE:
                object = (IRCCloudJSONObject) obj;
                b = BuffersList.getInstance().getBuffer(object.bid());
                if (b != null && adapter != null)
                    adapter.updateBuffer(b);
                break;
            case NetworkConnection.EVENT_BUFFERMSG:
                if (adapter != null) {
                    event = (Event) obj;
                    if (event.bid != selected_bid) {
                        Server s = ServersList.getInstance().getServer(event.cid);
                        if(s != null && s.collapsed != null && !mExpandCids.get(event.cid, false)) {
                            adapter.updateCollapsed(s);
                        } else {
                            b = BuffersList.getInstance().getBuffer(event.bid);
                            if (b != null && event.isImportant(b.getType()))
                                adapter.updateBuffer(b);
                        }
                    }
                }
                break;
            case NetworkConnection.EVENT_HEARTBEATECHO:
                if (adapter != null) {
                    object = (IRCCloudJSONObject) obj;
                    JsonNode seenEids = object.getJsonNode("seenEids");
                    Iterator<Map.Entry<String, JsonNode>> iterator = seenEids.fields();
                    int count = 0;
                    while (iterator.hasNext()) {
                        Map.Entry<String, JsonNode> entry = iterator.next();
                        JsonNode eids = entry.getValue();
                        Iterator<Map.Entry<String, JsonNode>> j = eids.fields();
                        while (j.hasNext()) {
                            Map.Entry<String, JsonNode> eidentry = j.next();
                            Integer bid = Integer.valueOf(eidentry.getKey());
                            b = BuffersList.getInstance().getBuffer(bid);
                            if (b != null)
                                adapter.updateBuffer(b);
                            count++;
                        }
                    }
                    if (count > 1) {
                        getActivity().runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                refresh();
                            }
                        });
                    }
                }
                break;
            case NetworkConnection.EVENT_JOIN:
            case NetworkConnection.EVENT_PART:
            case NetworkConnection.EVENT_QUIT:
            case NetworkConnection.EVENT_KICK:
                object = (IRCCloudJSONObject) obj;
                if (object.type().startsWith("you_") && getActivity() != null) {
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            refresh();
                        }
                    });
                }
                break;
            case NetworkConnection.EVENT_CHANNELTOPIC:
            case NetworkConnection.EVENT_NICKCHANGE:
            case NetworkConnection.EVENT_MEMBERUPDATES:
            case NetworkConnection.EVENT_USERCHANNELMODE:
            case NetworkConnection.EVENT_AWAY:
            case NetworkConnection.EVENT_SELFBACK:
            case NetworkConnection.EVENT_CHANNELTIMESTAMP:
            case NetworkConnection.EVENT_USERMODE:
            case NetworkConnection.EVENT_SETIGNORES:
            case NetworkConnection.EVENT_BADCHANNELKEY:
            case NetworkConnection.EVENT_OPENBUFFER:
            case NetworkConnection.EVENT_BANLIST:
            case NetworkConnection.EVENT_WHOLIST:
            case NetworkConnection.EVENT_WHOIS:
            case NetworkConnection.EVENT_LINKCHANNEL:
            case NetworkConnection.EVENT_LISTRESPONSEFETCHING:
            case NetworkConnection.EVENT_LISTRESPONSE:
            case NetworkConnection.EVENT_LISTRESPONSETOOMANY:
            case NetworkConnection.EVENT_GLOBALMSG:
            case NetworkConnection.EVENT_ACCEPTLIST:
            case NetworkConnection.EVENT_NAMESLIST:
            case NetworkConnection.EVENT_CHANNELTOPICIS:
            case NetworkConnection.EVENT_BACKLOG_FAILED:
            case NetworkConnection.EVENT_PROGRESS:
            case NetworkConnection.EVENT_ALERT:
            case NetworkConnection.EVENT_DEBUG:
            case NetworkConnection.EVENT_CONNECTIVITY:
            case NetworkConnection.EVENT_CACHE_START:
            case NetworkConnection.EVENT_OOB_START:
            case NetworkConnection.EVENT_OOB_FAILED:
            case NetworkConnection.EVENT_DISPLAYNAMECHANGE:
            case NetworkConnection.EVENT_AVATARCHANGE:
            case NetworkConnection.EVENT_MESSAGECHANGE:
                break;
            case NetworkConnection.EVENT_BACKLOG_START:
                if (refreshTask != null)
                    refreshTask.cancel(true);
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        progressBar.setVisibility((adapter == null || adapter.getItemCount() == 0) ? View.VISIBLE : View.GONE);
                    }
                });
                break;
            case NetworkConnection.EVENT_BACKLOG_END:
            case NetworkConnection.EVENT_CACHE_END:
                if(!requestingArchives && obj == null)
                    mExpandArchives.clear();
                requestingArchives = false;
                ready = true;
                Integer bid = (obj == null)?-1:((Integer) obj);
                b = BuffersList.getInstance().getBuffer(bid);
                if (obj != null && adapter != null && b != null) {
                    adapter.updateBuffer(b);
                    break;
                } else {
                    if (getActivity() != null) {
                        getActivity().runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                if (refreshTask != null)
                                    refreshTask.cancel(true);
                                IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "Refreshing buffers list");
                                refreshTask = new RefreshTask();
                                refreshTask.doInBackground((Void) null);
                                if(!refreshTask.isCancelled())
                                    refreshTask.onPostExecute(null);
                                IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "Done");
                            }
                        });
                    }
                }
                break;
            default:
                IRCCloudLog.Log(Log.WARN, "IRCCloud", "Slow event: " + what);
                if (getActivity() != null) {
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            refresh();
                        }
                    });
                }
                break;
        }
    }

    public void refresh() {
        if (refreshTask != null)
            refreshTask.cancel(true);
        refreshTask = new RefreshTask();
        refreshTask.execute((Void) null);
    }

    public void prev(boolean unread) {
        int pos = adapter.positionForBid(selected_bid);
        if(unread)
            pos = adapter.unreadPositionAbove(pos);
        else
            pos--;

        Buffer b;
        do {
            if(pos >= 0)
                b = (Buffer) adapter.getItem(pos);
            else
                b = null;
            pos--;
        } while(b != null && (b.getType().equals(Buffer.TYPE_ARCHIVES_HEADER) || b.getType().equals(Buffer.TYPE_JOIN_CHANNEL)));
        if(b != null)
            mListener.onBufferSelected(b.getBid());
    }

    public void next(boolean unread) {
        int pos = adapter.positionForBid(selected_bid);
        if(unread)
            pos = adapter.unreadPositionBelow(pos);
        else
            pos++;

        Buffer b;
        do {
            if(pos < adapter.getItemCount())
                b = (Buffer) adapter.getItem(pos);
            else
                b = null;
            pos++;
        } while(b != null && (b.getType().equals(Buffer.TYPE_ARCHIVES_HEADER) || b.getType().equals(Buffer.TYPE_JOIN_CHANNEL)));
        if(b != null)
            mListener.onBufferSelected(b.getBid());
    }

    public Resources getSafeResources() {
        return IRCCloudApplication.getInstance().getApplicationContext().getResources();
    }

    public interface OnBufferSelectedListener {
        void onBufferSelected(int bid);

        boolean onBufferLongClicked(Buffer b);

        void addButtonPressed(int cid);

        void addNetwork();

        void reorder();
    }
} public class ChannelListFragment extends DialogFragment implements NetworkConnection.IRCEventHandler {
    ArrayList<ChannelRow> channels;
    ChannelsAdapter adapter = new ChannelsAdapter();
    NetworkConnection conn;
    RecyclerView recyclerView;
    TextView empty;
    Server server;

    public static class ChannelRow implements Parcelable {
        public Spanned name;
        public Spanned topic;

        public ChannelRow() {

        }

        protected ChannelRow(Parcel in) {
            name = (Spanned) in.readValue(Spanned.class.getClassLoader());
            topic = (Spanned) in.readValue(Spanned.class.getClassLoader());
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeValue(name);
            dest.writeValue(topic);
        }

        @SuppressWarnings("unused")
        public static final Parcelable.Creator<ChannelRow> CREATOR = new Parcelable.Creator<ChannelRow>() {
            @Override
            public ChannelRow createFromParcel(Parcel in) {
                return new ChannelRow(in);
            }

            @Override
            public ChannelRow[] newArray(int size) {
                return new ChannelRow[size];
            }
        };
    }

    private class ViewHolder extends RecyclerView.ViewHolder {
        public RowChannelBinding binding;

        public ViewHolder(View v) {
            super(v);
            binding = DataBindingUtil.bind(v);
        }
    }

    private class ChannelsAdapter extends RecyclerView.Adapter<ViewHolder> {

        public void set(JsonNode json) {
            channels = new ArrayList<>(json.size());

            for (int i = 0; i < json.size(); i++) {
                ChannelRow c = new ChannelRow();
                JsonNode o = json.get(i);
                String channel = o.get("name").asText() + " (" + o.get("num_members").asInt() + " member";
                if (o.get("num_members").asInt() != 1)
                    channel += "s";
                channel += ")";
                c.name = ColorFormatter.html_to_spanned(channel, true, server);

                String topic = o.get("topic").asText();
                if (topic.length() > 0) {
                    c.topic = ColorFormatter.html_to_spanned(topic, true, server);
                } else {
                    c.topic = null;
                }
                channels.add(c);
            }
        }

        @Override
        public int getItemCount() {
            if (channels == null)
                return 0;
            else
                return channels.size();
        }

        @Override
        public long getItemId(int pos) {
            return pos;
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = RowChannelBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false).getRoot();
            return new ViewHolder(v);
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, final int position) {
            RowChannelBinding row = holder.binding;
            ChannelRow c = channels.get(position);

            row.setChannel(c);
            row.name.setMovementMethod(IRCCloudLinkMovementMethod.getInstance());
            row.topic.setMovementMethod(IRCCloudLinkMovementMethod.getInstance());
            row.executePendingBindings();
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Context ctx = getActivity();
        if(ctx == null)
            return null;
        if(server == null && savedInstanceState != null && savedInstanceState.containsKey("cid"))
            server = ServersList.getInstance().getServer(savedInstanceState.getInt("cid"));
        if(server == null)
            return null;

        if(savedInstanceState != null && savedInstanceState.containsKey("channels"))
            channels = savedInstanceState.getParcelableArrayList("channels");

        LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(R.layout.recyclerview, null);
        recyclerView = v.findViewById(R.id.recycler);
        recyclerView.setLayoutManager(new LinearLayoutManager(v.getContext()));
        recyclerView.setAdapter(adapter);
        empty = v.findViewById(android.R.id.empty);
        empty.setText("Loading channel list…");
        Dialog d = new AlertDialog.Builder(ctx)
                .setTitle("List of channels on " + server.getHostname())
                .setView(v)
                .setNegativeButton("Close", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                })
                .create();
        return d;
    }

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);

        if(server != null)
            outState.putInt("cid", server.getCid());

        if(channels != null && channels.size() > 0)
            outState.putParcelableArrayList("channels", channels);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (conn != null)
            conn.removeHandler(this);
    }

    @Override
    public void setArguments(Bundle args) {
        if(args != null) {
            server = ServersList.getInstance().getServer(args.getInt("cid", -1));
            channels = null;
            if (recyclerView != null && getActivity() != null) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        getDialog().setTitle("List of channels on " + server.getHostname());
                        if (adapter.getItemCount() > 0) {
                            empty.setVisibility(View.GONE);
                            recyclerView.setVisibility(View.VISIBLE);
                        } else {
                            empty.setVisibility(View.VISIBLE);
                            recyclerView.setVisibility(View.GONE);
                        }
                        adapter.notifyDataSetChanged();
                    }
                });
            }
        }
    }

    public void onResume() {
        super.onResume();
        if(adapter.getItemCount() > 0) {
            empty.setVisibility(View.GONE);
            recyclerView.setVisibility(View.VISIBLE);
        } else {
            empty.setVisibility(View.VISIBLE);
            recyclerView.setVisibility(View.GONE);
        }
        adapter.notifyDataSetChanged();
    }

    public void onIRCEvent(int what, Object obj) {
        final IRCCloudJSONObject o;
        switch (what) {
            case NetworkConnection.EVENT_LISTRESPONSE:
                o = (IRCCloudJSONObject) obj;
                server = ServersList.getInstance().getServer(o.cid());
                adapter.set(o.getJsonNode("channels"));
                if (getActivity() != null)
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            getDialog().setTitle("List of channels on " + server.getHostname());
                            adapter.notifyDataSetChanged();
                            if(adapter.getItemCount() > 0) {
                                empty.setVisibility(View.GONE);
                                recyclerView.setVisibility(View.VISIBLE);
                            } else {
                                empty.setVisibility(View.VISIBLE);
                                recyclerView.setVisibility(View.GONE);
                            }
                        }
                    });
                break;
            case NetworkConnection.EVENT_LISTRESPONSETOOMANY:
                if (getActivity() != null)
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            empty.setText("Too many channels to list.  Try limiting the list to only respond with channels that have more than e.g. 50 members: /LIST >50");
                        }
                    });
                break;
            default:
                break;
        }
    }
} public class ChannelModeListFragment extends DialogFragment implements NetworkConnection.IRCEventHandler {
    JsonNode data;
    int cid;
    int bid = -1;
    IRCCloudJSONObject event;
    Adapter adapter;
    NetworkConnection conn;
    RecyclerView recyclerView;
    TextView empty;
    String mode;
    String placeholder;
    String mask;
    String list;
    String title;
    boolean canChangeMode = false;

    private class ViewHolder extends RecyclerView.ViewHolder {
        public RowChannelmodeBinding binding;

        public ViewHolder(View v) {
            super(v);
            binding = DataBindingUtil.bind(v);
        }
    }

    private class Adapter extends RecyclerView.Adapter<ViewHolder> {

        @Override
        public int getItemCount() {
            return data.size();
        }

        @Override
        public long getItemId(int pos) {
            return pos;
        }

        OnClickListener removeClickListener = new OnClickListener() {
            @Override
            public void onClick(View v) {
                Integer position = (Integer) v.getTag();
                try {
                    conn.mode(cid, event.getString("channel"), "-" + mode + " " + data.get(position).get(mask).asText(), null);
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    NetworkConnection.printStackTraceToCrashlytics(e);
                }
            }
        };

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = RowChannelmodeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false).getRoot();
            return new ViewHolder(v);
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, final int position) {
            RowChannelmodeBinding row = holder.binding;
            JsonNode node = data.get(position);
            row.setMask(Html.fromHtml(node.get(mask).asText()));
            if(node.has("usermask") && node.get("usermask") != null && node.get("usermask").asText() != null)
                row.setSetBy("Set " + DateUtils.getRelativeTimeSpanString(node.get("time").asLong() * 1000L, System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS)
                    + " by " + node.get("usermask").asText());
            else if(node.has("author") && node.get("author") != null && node.get("author").asText() != null)
                row.setSetBy("Set " + DateUtils.getRelativeTimeSpanString(node.get("time").asLong() * 1000L, System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS)
                        + " by " + node.get("author").asText());
            else
                row.setSetBy("");
            if (canChangeMode) {
                row.removeBtn.setVisibility(View.VISIBLE);
                row.removeBtn.setOnClickListener(removeClickListener);
                row.removeBtn.setColorFilter(ColorScheme.getInstance().colorControlNormal, PorterDuff.Mode.SRC_ATOP);
                row.removeBtn.setTag(position);
            } else {
                row.removeBtn.setVisibility(View.GONE);
            }
            row.executePendingBindings();
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Context ctx = getActivity();
        if (ctx == null)
            return null;

        LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(R.layout.recyclerview, null);
        recyclerView = v.findViewById(R.id.recycler);
        recyclerView.setLayoutManager(new LinearLayoutManager(v.getContext()));
        empty = v.findViewById(android.R.id.empty);
        empty.setText(placeholder);
        if (savedInstanceState != null && savedInstanceState.containsKey("cid")) {
            cid = savedInstanceState.getInt("cid");
            bid = savedInstanceState.getInt("bid");
            event = new IRCCloudJSONObject(savedInstanceState.getString("event"));
            list = savedInstanceState.getString("list");
            mask = savedInstanceState.getString("mask");
            placeholder = savedInstanceState.getString("placeholder");
            title = savedInstanceState.getString("title");
            mode = savedInstanceState.getString("mode");
            data = event.getJsonNode(list);
            adapter = new Adapter();
            recyclerView.setAdapter(adapter);
            if(adapter.getItemCount() > 0) {
                empty.setVisibility(View.GONE);
                recyclerView.setVisibility(View.VISIBLE);
            } else {
                empty.setVisibility(View.VISIBLE);
                recyclerView.setVisibility(View.GONE);
            }
        }
        AlertDialog.Builder b = new AlertDialog.Builder(ctx)
                .setTitle(title)
                .setView(v)
                .setNegativeButton("Close", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                });
        try {
            if (canChangeMode) {
                b.setPositiveButton("Add", new AddClickListener());
            }
        } catch (Exception e) {

        }

        AlertDialog d = b.create();
        d.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
        return d;
    }

    class AddClickListener implements DialogInterface.OnClickListener {
        @Override
        public void onClick(DialogInterface d, int which) {
            Context ctx = getActivity();

            ServersList s = ServersList.getInstance();
            Server server = s.getServer(cid);
            AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
            LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            View view = inflater.inflate(R.layout.dialog_textprompt, null);
            TextView prompt = view.findViewById(R.id.prompt);
            final EditText input = view.findViewById(R.id.textInput);
            if(event.getString("type").equals("chanfilter_list")) {
                input.setHint("\\*key\\*word\\*");
                prompt.setText("Add this pattern");
            } else {
                input.setHint("nickname!user@host.name");
                prompt.setText("Add this hostmask");
            }
            builder.setTitle(server.getName() + " (" + server.getHostname() + ":" + (server.getPort()) + ")");
            builder.setView(view);
            builder.setPositiveButton("Add", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    conn.mode(cid, event.getString("channel"), "+" + mode + " " + input.getText().toString(), null);
                    dialog.dismiss();
                }
            });
            builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.dismiss();
                }
            });
            AlertDialog dialog = builder.create();
            dialog.setOwnerActivity(getActivity());
            dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
            dialog.show();
        }
    }


    @Override
    public void onSaveInstanceState(Bundle state) {
        state.putInt("cid", cid);
        state.putInt("bid", bid);
        state.putString("event", event.toString());
        state.putString("list", list);
        state.putString("mask", mask);
        state.putString("placeholder", placeholder);
        state.putString("title", title);
        state.putString("mode", mode);
    }

    @Override
    public void setArguments(Bundle args) {
        if(args != null) {
            cid = args.getInt("cid", -1);
            bid = args.getInt("bid", -1);
            event = new IRCCloudJSONObject(args.getString("event"));
            list = args.getString("list");
            mask = args.getString("mask");
            placeholder = args.getString("placeholder");
            title = args.getString("title");
            mode = args.getString("mode");
            data = event.getJsonNode(list);
            Server s = ServersList.getInstance().getServer(cid);
            if (s != null) {
                User self_user = UsersList.getInstance().getUser(bid, s.getNick());
                canChangeMode = (self_user != null && (self_user.mode.contains(s.MODE_OWNER) || self_user.mode.contains(s.MODE_ADMIN) || self_user.mode.contains(s.MODE_OP) || self_user.mode.contains(s.MODE_HALFOP)));
            } else {
                canChangeMode = false;
            }
            if (getActivity() != null && cid > 0 && recyclerView != null) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (adapter == null) {
                            adapter = new Adapter();
                            recyclerView.setAdapter(adapter);
                        } else {
                            adapter.notifyDataSetChanged();
                        }
                        if (adapter.getItemCount() > 0) {
                            empty.setVisibility(View.GONE);
                            recyclerView.setVisibility(View.VISIBLE);
                        } else {
                            empty.setVisibility(View.VISIBLE);
                            recyclerView.setVisibility(View.GONE);
                        }
                    }
                });
            }
        }
    }

    public void onResume() {
        super.onResume();

        if (cid > 0) {
            adapter = new Adapter();
            recyclerView.setAdapter(adapter);
            if(adapter.getItemCount() > 0) {
                empty.setVisibility(View.GONE);
                recyclerView.setVisibility(View.VISIBLE);
            } else {
                empty.setVisibility(View.VISIBLE);
                recyclerView.setVisibility(View.GONE);
            }
        }
        Server s = ServersList.getInstance().getServer(cid);
        if (s != null) {
            User self_user = UsersList.getInstance().getUser(bid, s.getNick());
            canChangeMode = (self_user != null && (self_user.mode.contains(s.MODE_OWNER) || self_user.mode.contains(s.MODE_ADMIN) || self_user.mode.contains(s.MODE_OP) || self_user.mode.contains(s.MODE_HALFOP)));
        } else {
            canChangeMode = false;
        }
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        conn = NetworkConnection.getInstance();
        conn.addHandler(this);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (conn != null)
            conn.removeHandler(this);
    }

    public void onIRCEvent(int what, Object obj) {
        switch (what) {
            case NetworkConnection.EVENT_USERCHANNELMODE:
                Server s = ServersList.getInstance().getServer(cid);
                User self_user = UsersList.getInstance().getUser(bid, s.getNick());
                canChangeMode = (self_user != null && (self_user.mode.contains(s.MODE_OWNER) || self_user.mode.contains(s.MODE_ADMIN) || self_user.mode.contains(s.MODE_OP) || self_user.mode.contains(s.MODE_HALFOP)));
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (adapter != null)
                            adapter.notifyDataSetChanged();
                    }
                });
                break;
            case NetworkConnection.EVENT_BUFFERMSG:
                Event e = (Event) obj;
                if (e.bid == bid && e.type.equals("channel_mode_list_change"))
                    conn.mode(cid, event.getString("channel"), mode, null);
                break;
            default:
                break;
        }
    }
} public class EditConnectionFragment extends DialogFragment {
    private class PresetServersAdapter extends BaseAdapter {
        private Activity ctx;

        private class ViewHolder {
            TextView network;
            TextView hostname;
            TextView lock;
        }

        private class PresetServer {
            String network;
            String host;
            int port;

            public PresetServer(String network, String host, int port) {
                this.network = network;
                this.host = host;
                this.port = port;
            }
        }

        private ArrayList<PresetServer> data;

        public PresetServersAdapter(Activity context, JSONArray networks) {
            ctx = context;
            data = new ArrayList<PresetServer>();
            data.add(new PresetServer(null, null, 0));
            for (int i = 0; i < networks.length(); i++) {
                try {
                    JSONObject o = networks.getJSONObject(i);
                    String name = o.getString("name");
                    JSONObject server = o.getJSONArray("servers").getJSONObject(0);
                    data.add(new PresetServer(name, server.getString("hostname"), server.getInt("port")));
                } catch (JSONException e) {
                }
            }
        }

        public PresetServersAdapter(Activity context) {
            ctx = context;
            data = new ArrayList<PresetServer>();
            data.add(new PresetServer(null, null, 0));
        }

        @Override
        public int getCount() {
            return data.size();
        }

        @Override
        public Object getItem(int pos) {
            return data.get(pos);
        }

        @Override
        public long getItemId(int pos) {
            return pos;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View row = convertView;
            TextView label;

            if (row == null) {
                LayoutInflater inflater = ctx.getLayoutInflater();
                row = inflater.inflate(android.R.layout.simple_spinner_item, null);

                label = row.findViewById(android.R.id.text1);
                row.setTag(label);
            } else {
                label = (TextView) row.getTag();
            }

            if (position == 0) {
                label.setText("Choose a network…");
            } else {
                label.setText(data.get(position).network);
            }

            return row;
        }

        @Override
        public View getDropDownView(int position, View convertView, ViewGroup parent) {
            View row = convertView;
            ViewHolder holder;

            if (row == null) {
                LayoutInflater inflater = ctx.getLayoutInflater();
                row = inflater.inflate(R.layout.row_server, parent, false);

                holder = new ViewHolder();
                holder.network = row.findViewById(R.id.network);
                holder.hostname = row.findViewById(R.id.hostname);
                holder.lock = row.findViewById(R.id.lock);
                holder.lock.setTypeface(FontAwesome.getTypeface());

                row.setTag(holder);
            } else {
                holder = (ViewHolder) row.getTag();
            }

            if (position == 0) {
                holder.network.setText("Choose a network…");
                holder.hostname.setVisibility(View.GONE);
                holder.lock.setVisibility(View.GONE);
            } else {
                PresetServer s = data.get(position);
                holder.network.setText(s.network);
                holder.hostname.setText(s.host);
                holder.hostname.setVisibility(View.VISIBLE);
                holder.lock.setVisibility(View.VISIBLE);
                if (s.port == 6697) {
                    holder.lock.setText(FontAwesome.SHIELD);
                } else {
                    holder.lock.setText(FontAwesome.GLOBE);
                }
            }
            return row;
        }
    }

    PresetServersAdapter adapter;

    Server server;

    LinearLayout channelsWrapper;
    Spinner presets;
    EditText hostname;
    EditText port;
    SwitchCompat ssl;
    EditText nickname;
    EditText realname;
    EditText channels;
    EditText nickserv_pass;
    EditText join_commands;
    EditText server_pass;
    EditText network;
    LinearLayout connection_settings;

    public String default_hostname = null;
    public int default_port = 6667;
    public String default_channels = null;

    private void init(View v) {
        channelsWrapper = v.findViewById(R.id.channels_wrapper);
        presets = v.findViewById(R.id.presets);
        hostname = v.findViewById(R.id.hostname);
        port = v.findViewById(R.id.port);
        ssl = v.findViewById(R.id.ssl);
        nickname = v.findViewById(R.id.nickname);
        realname = v.findViewById(R.id.realname);
        channels = v.findViewById(R.id.channels);
        nickserv_pass = v.findViewById(R.id.nickservpassword);
        join_commands = v.findViewById(R.id.commands);
        server_pass = v.findViewById(R.id.serverpassword);
        network = v.findViewById(R.id.network);
        connection_settings = v.findViewById(R.id.connection_settings);

        presets.setOnItemSelectedListener(new OnItemSelectedListener() {

            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                if (position == 0) {
                    if (server != null) {
                        hostname.setText(server.getHostname());
                        port.setText(String.valueOf(server.getPort()));
                        ssl.setChecked(server.getSsl() == 1);
                    } else {
                        if (default_hostname != null)
                            hostname.setText(default_hostname);
                        else
                            hostname.setText("");
                        port.setText(String.valueOf(default_port));
                        ssl.setChecked(default_port == 6697);
                    }
                } else {
                    PresetServersAdapter.PresetServer s = (PresetServersAdapter.PresetServer) adapter.getItem(position);
                    hostname.setText(s.host);
                    port.setText(String.valueOf(s.port));
                    ssl.setChecked(s.port == 6697);
                }
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {
                if (server != null) {
                    hostname.setText(server.getHostname());
                    port.setText(String.valueOf(server.getPort()));
                    ssl.setChecked(server.getSsl() == 1);
                } else {
                    if (default_hostname != null)
                        hostname.setText(default_hostname);
                    else
                        hostname.setText("");
                    port.setText(String.valueOf(default_port));
                    ssl.setChecked(default_port == 6697);
                }
            }

        });

        if (NetworkConnection.getInstance().getUserInfo() != null && !NetworkConnection.getInstance().getUserInfo().verified) {
            Button b = v.findViewById(R.id.resend);
            b.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    NetworkConnection.getInstance().resend_verify_email(null);
                    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
                    builder.setTitle("Confirmation Sent");
                    builder.setMessage("You should shortly receive an email with a link to confirm your address.");
                    builder.setNeutralButton("Close", null);
                    builder.show();
                }
            });
            v.findViewById(R.id.unverified).setVisibility(View.VISIBLE);
        }
    }

    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        if (getShowsDialog()) {
            return super.onCreateView(inflater, container, savedInstanceState);
        } else {
            final View v = inflater.inflate(R.layout.dialog_edit_connection, container, false);
            init(v);
            return v;
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Context ctx = getActivity();
        if(ctx == null)
            return null;

        LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(R.layout.dialog_edit_connection, null);
        init(v);

        if (savedInstanceState != null && savedInstanceState.containsKey("cid"))
            server = ServersList.getInstance().getServer(savedInstanceState.getInt("cid"));

        final AlertDialog d = new AlertDialog.Builder(ctx)
                .setTitle((server == null) ? "Add A Network" : "Edit Connection")
                .setView(v)
                .setPositiveButton((server == null) ? "Add" : "Save", null)
                .setNegativeButton("Cancel", null)
                .create();
        d.setOnShowListener(new DialogInterface.OnShowListener() {
            @Override
            public void onShow(DialogInterface dialogInterface) {
                Button b = d.getButton(AlertDialog.BUTTON_POSITIVE);
                b.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        save(null);
                    }
                });
            }
        });
        d.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
        return d;
    }

    public void setCid(int cid) {
        server = ServersList.getInstance().getServer(cid);
    }

    @Override
    public void onResume() {
        super.onResume();

        if (server != null) {
            connection_settings.setVisibility(server.getSlack() == 1 ? View.GONE : View.VISIBLE);
            presets.setSelection(0);
            presets.setVisibility(View.GONE);
            network.setVisibility(View.VISIBLE);
            network.setText(server.getName());
            channelsWrapper.setVisibility(View.GONE);
            hostname.setText(server.getHostname());
            port.setText(String.valueOf(server.getPort()));
            ssl.setChecked(server.getSsl() > 0);
            nickname.setText(server.getNick());
            realname.setText(server.getRealname());
            join_commands.setText(server.getJoin_commands());
            nickserv_pass.setText(server.getNickserv_pass());
            server_pass.setText(server.getServer_pass());
        } else {
            if (adapter == null)
                new LoadNetworkPresets().execute((Void) null);

            if (default_hostname != null)
                hostname.setText(default_hostname);
            port.setText(String.valueOf(default_port));
            ssl.setChecked(default_port == 6697);
            if (default_channels != null)
                channels.setText(default_channels);
            if (NetworkConnection.getInstance().getUserInfo() != null) {
                String name = NetworkConnection.getInstance().getUserInfo().name;
                String nick;
                if (name.contains(" ")) {
                    nick = name.substring(0, name.indexOf(" "));
                } else {
                    nick = name;
                }
                realname.setText(name);
                nickname.setText(nick.toLowerCase());
            }
        }
    }

    @Override public void onDestroy() {
        super.onDestroy();
        if(presets != null)
            presets.setOnItemSelectedListener(null);
    }

    @Override
    public void onSaveInstanceState(Bundle savedInstanceState) {
        if (server != null)
            savedInstanceState.putInt("cid", server.getCid());
    }

    public void save(final NetworkConnection.IRCResultCallback callback) {
        NetworkConnection.IRCResultCallback c = new NetworkConnection.IRCResultCallback() {
            @Override
            public void onIRCResult(IRCCloudJSONObject result) {
                if(result.getBoolean("success")) {
                    try {
                        dismiss();
                    } catch (Exception e) {
                    }
                } else {
                    final String message = result.getString("message");
                    if (getActivity() != null)
                        getActivity().runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                switch (message) {
                                    case "passworded_servers":
                                        Toast.makeText(IRCCloudApplication.getInstance().getApplicationContext(), "You can’t connect to passworded servers with free accounts.", Toast.LENGTH_LONG).show();
                                        break;
                                    case "networks":
                                        Toast.makeText(IRCCloudApplication.getInstance().getApplicationContext(), "You've exceeded the connection limit for free accounts.", Toast.LENGTH_LONG).show();
                                        break;
                                    case "unverified":
                                        Toast.makeText(IRCCloudApplication.getInstance().getApplicationContext(), "You can’t connect to external servers until you confirm your email address.", Toast.LENGTH_LONG).show();
                                        break;
                                    case "sts_policy":
                                        Toast.makeText(IRCCloudApplication.getInstance().getApplicationContext(), "You can’t disable secure connections to this network because it’s using a strict transport security policy.", Toast.LENGTH_LONG).show();
                                        break;
                                    default:
                                        Toast.makeText(IRCCloudApplication.getInstance().getApplicationContext(), "Unable to add connection: invalid " + message, Toast.LENGTH_LONG).show();
                                        break;
                                }
                            }
                        });
                }

                if(callback != null)
                    callback.onIRCResult(result);
            }
        };

        int portValue = 6667;
        try {
            portValue = Integer.parseInt(port.getText().toString());
        } catch (NumberFormatException e) {
        }
        if (server == null) {
            String netname = null;
            if (presets.getSelectedItemPosition() > 0) {
                netname = ((PresetServersAdapter.PresetServer) adapter.getItem(presets.getSelectedItemPosition())).network;
            }
            NetworkConnection.getInstance().addServer(hostname.getText().toString(), portValue,
                    ssl.isChecked() ? 1 : 0, netname, nickname.getText().toString(), realname.getText().toString(), server_pass.getText().toString(),
                    nickserv_pass.getText().toString(), join_commands.getText().toString(), channels.getText().toString(), c);
        } else {
            String netname = network.getText().toString();
            if(server.getSlack() == 1) {
                NetworkConnection.getInstance().set_netname(server.getCid(), netname, c);
            } else {
                if (hostname.getText().toString().equalsIgnoreCase(netname))
                    netname = null;
                NetworkConnection.getInstance().editServer(server.getCid(), hostname.getText().toString(), portValue,
                        ssl.isChecked() ? 1 : 0, netname, nickname.getText().toString(), realname.getText().toString(), server_pass.getText().toString(),
                        nickserv_pass.getText().toString(), join_commands.getText().toString(), c);
            }
        }
    }

    private class LoadNetworkPresets extends AsyncTaskEx<Void, Void, JSONArray> {

        @Override
        protected void onPreExecute() {
            presets.setEnabled(false);
            presets.setAdapter(new ArrayAdapter<String>(getActivity(), R.layout.loading_spinner_item, R.id.text, new String[]{"Loading networks"}));
        }

        @Override
        protected JSONArray doInBackground(Void... params) {
            try {
                return NetworkConnection.getInstance().networkPresets();
            } catch (IOException e) {
                return null;
            }
        }

        @Override
        protected void onPostExecute(JSONArray networks) {
            if (networks != null && networks.length() > 0) {
                adapter = new PresetServersAdapter(getActivity(), networks);
            } else {
                adapter = new PresetServersAdapter(getActivity());
            }
            presets.setAdapter(adapter);
            presets.setEnabled(true);
        }
    }
} public class IRCColorPickerFragment extends Fragment {
    public static final String ARG_BACKGROUND = "background";
    private boolean mBackground;

    private OnColorPickedListener mListener;

    public IRCColorPickerFragment() {
        // Required empty public constructor
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mBackground = getArguments().getBoolean(ARG_BACKGROUND);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View.OnClickListener clickListener = new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(mListener != null)
                    mListener.onColorPicked((int)view.getTag(), mBackground);
            }
        };

        View v = inflater.inflate(R.layout.irccolorpicker, container, false);

        v.findViewById(R.id.color0).setOnClickListener(clickListener);
        v.findViewById(R.id.color1).setOnClickListener(clickListener);
        v.findViewById(R.id.color2).setOnClickListener(clickListener);
        v.findViewById(R.id.color3).setOnClickListener(clickListener);
        v.findViewById(R.id.color4).setOnClickListener(clickListener);
        v.findViewById(R.id.color5).setOnClickListener(clickListener);
        v.findViewById(R.id.color6).setOnClickListener(clickListener);
        v.findViewById(R.id.color7).setOnClickListener(clickListener);
        v.findViewById(R.id.color8).setOnClickListener(clickListener);
        v.findViewById(R.id.color9).setOnClickListener(clickListener);
        v.findViewById(R.id.color10).setOnClickListener(clickListener);
        v.findViewById(R.id.color11).setOnClickListener(clickListener);
        v.findViewById(R.id.color12).setOnClickListener(clickListener);
        v.findViewById(R.id.color13).setOnClickListener(clickListener);
        v.findViewById(R.id.color14).setOnClickListener(clickListener);
        v.findViewById(R.id.color15).setOnClickListener(clickListener);

        return v;
    }

    @Override
    public void setArguments(Bundle args) {
        super.setArguments(args);
        if(args != null)
            mBackground = args.getBoolean(ARG_BACKGROUND);
        updateColors();
    }

    public boolean isBackground() {
        return mBackground;
    }

    @Override
    public void onResume() {
        super.onResume();
        updateColors();
    }

    private void updateColors() {
        View v = getView();

        if(v != null) {
            Drawable d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(0, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color0)).setBackgroundDrawable(d);
            (v.findViewById(R.id.color0)).setTag(ColorScheme.getIRCColor(0, mBackground));

            d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(1, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color1)).setBackgroundDrawable(d.mutate());
            (v.findViewById(R.id.color1)).setTag(ColorScheme.getIRCColor(1, mBackground));

            d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(2, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color2)).setBackgroundDrawable(d.mutate());
            (v.findViewById(R.id.color2)).setTag(ColorScheme.getIRCColor(2, mBackground));

            d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(3, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color3)).setBackgroundDrawable(d.mutate());
            (v.findViewById(R.id.color3)).setTag(ColorScheme.getIRCColor(3, mBackground));

            d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(4, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color4)).setBackgroundDrawable(d.mutate());
            (v.findViewById(R.id.color4)).setTag(ColorScheme.getIRCColor(4, mBackground));

            d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(5, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color5)).setBackgroundDrawable(d.mutate());
            (v.findViewById(R.id.color5)).setTag(ColorScheme.getIRCColor(5, mBackground));

            d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(6, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color6)).setBackgroundDrawable(d.mutate());
            (v.findViewById(R.id.color6)).setTag(ColorScheme.getIRCColor(6, mBackground));

            d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(7, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color7)).setBackgroundDrawable(d.mutate());
            (v.findViewById(R.id.color7)).setTag(ColorScheme.getIRCColor(7, mBackground));

            d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(8, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color8)).setBackgroundDrawable(d.mutate());
            (v.findViewById(R.id.color8)).setTag(ColorScheme.getIRCColor(8, mBackground));

            d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(9, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color9)).setBackgroundDrawable(d.mutate());
            (v.findViewById(R.id.color9)).setTag(ColorScheme.getIRCColor(9, mBackground));

            d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(10, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color10)).setBackgroundDrawable(d.mutate());
            (v.findViewById(R.id.color10)).setTag(ColorScheme.getIRCColor(10, mBackground));

            d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(11, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color11)).setBackgroundDrawable(d.mutate());
            (v.findViewById(R.id.color11)).setTag(ColorScheme.getIRCColor(11, mBackground));

            d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(12, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color12)).setBackgroundDrawable(d.mutate());
            (v.findViewById(R.id.color12)).setTag(ColorScheme.getIRCColor(12, mBackground));

            d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(13, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color13)).setBackgroundDrawable(d.mutate());
            (v.findViewById(R.id.color13)).setTag(ColorScheme.getIRCColor(13, mBackground));

            d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(14, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color14)).setBackgroundDrawable(d.mutate());
            (v.findViewById(R.id.color14)).setTag(ColorScheme.getIRCColor(14, mBackground));

            d = VectorDrawableCompat.create(getActivity().getResources(), R.drawable.circle, null).mutate();
            d.setColorFilter(ColorScheme.getIRCColor(15, mBackground), PorterDuff.Mode.SRC_ATOP);
            (v.findViewById(R.id.color15)).setBackgroundDrawable(d.mutate());
            (v.findViewById(R.id.color15)).setTag(ColorScheme.getIRCColor(15, mBackground));
        }
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof OnColorPickedListener) {
            mListener = (OnColorPickedListener) context;
        } else {
            throw new RuntimeException(context.toString()
                    + " must implement OnColorPickedListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mListener = null;
    }

    public interface OnColorPickedListener {
        void onColorPicked(int color, boolean background);
    }
} public class IgnoreListFragment extends DialogFragment implements NetworkConnection.IRCEventHandler {
    JsonNode ignores;
    int cid;
    IgnoresAdapter adapter;
    NetworkConnection conn;
    RecyclerView recyclerView;
    TextView empty;

    private class ViewHolder extends RecyclerView.ViewHolder {
        public RowHostmaskBinding binding;

        public ViewHolder(View v) {
            super(v);
            binding = DataBindingUtil.bind(v);
        }
    }

    private class IgnoresAdapter extends RecyclerView.Adapter<ViewHolder> {
        @Override
        public int getItemCount() {
            return ignores.size();
        }

        @Override
        public long getItemId(int pos) {
            return pos;
        }

        OnClickListener removeClickListener = new OnClickListener() {
            @Override
            public void onClick(View v) {
                Integer position = (Integer) v.getTag();
                try {
                    conn.unignore(cid, ignores.get(position).asText(), null);
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    NetworkConnection.printStackTraceToCrashlytics(e);
                }
            }
        };

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = RowHostmaskBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false).getRoot();
            return new ViewHolder(v);
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, final int position) {
            RowHostmaskBinding row = holder.binding;
            row.setLabel(ignores.get(position).asText());
            row.removeBtn.setOnClickListener(removeClickListener);
            row.removeBtn.setColorFilter(ColorScheme.getInstance().colorControlNormal, PorterDuff.Mode.SRC_ATOP);
            row.removeBtn.setTag(position);
            row.executePendingBindings();
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Context ctx = getActivity();
        if(ctx == null)
            return null;

        LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(R.layout.recyclerview, null);
        recyclerView = v.findViewById(R.id.recycler);
        recyclerView.setLayoutManager(new LinearLayoutManager(v.getContext()));
        empty = v.findViewById(android.R.id.empty);
        empty.setText("You're not ignoring anyone at the moment.\n\nYou can ignore someone by tapping their nickname in the user list, long-pressing a message, or by using /ignore.");
        if (savedInstanceState != null && savedInstanceState.containsKey("cid")) {
            cid = savedInstanceState.getInt("cid");
            ignores = ServersList.getInstance().getServer(cid).raw_ignores;
            adapter = new IgnoresAdapter();
            recyclerView.setAdapter(adapter);
            if(adapter.getItemCount() > 0) {
                empty.setVisibility(View.GONE);
                recyclerView.setVisibility(View.VISIBLE);
            } else {
                empty.setVisibility(View.VISIBLE);
                recyclerView.setVisibility(View.GONE);
            }
        }
        Dialog d = new AlertDialog.Builder(ctx)
                .setTitle("Ignore list for " + ServersList.getInstance().getServer(cid).getName())
                .setView(v)
                .setPositiveButton("Add Ignore Mask", new AddClickListener())
                .setNegativeButton("Close", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                })
                .create();
        d.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
        return d;
    }

    class AddClickListener implements DialogInterface.OnClickListener {
        @Override
        public void onClick(DialogInterface d, int which) {
            Context ctx = getActivity();

            ServersList s = ServersList.getInstance();
            Server server = s.getServer(cid);
            AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
            LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            View view = inflater.inflate(R.layout.dialog_textprompt, null);
            TextView prompt = view.findViewById(R.id.prompt);
            final EditText input = view.findViewById(R.id.textInput);
            input.setHint("nickname!user@host.name");
            prompt.setText("Ignore messages from this hostmask");
            builder.setTitle(server.getName() + " (" + server.getHostname() + ":" + (server.getPort()) + ")");
            builder.setView(view);
            builder.setPositiveButton("Ignore", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    conn.ignore(cid, input.getText().toString(), null);
                    dialog.dismiss();
                }
            });
            builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.dismiss();
                }
            });
            AlertDialog dialog = builder.create();
            dialog.setOwnerActivity(getActivity());
            dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
            dialog.show();
        }
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        conn = NetworkConnection.getInstance();
        conn.addHandler(this);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if(conn != null)
            conn.removeHandler(this);
    }

    @Override
    public void onSaveInstanceState(Bundle state) {
        state.putInt("cid", cid);
    }

    @Override
    public void setArguments(Bundle args) {
        if(args != null) {
            cid = args.getInt("cid", 0);
            if (getActivity() != null && cid > 0 && recyclerView != null) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        ignores = ServersList.getInstance().getServer(cid).raw_ignores;
                        adapter = new IgnoresAdapter();
                        recyclerView.setAdapter(adapter);
                        if (adapter.getItemCount() > 0) {
                            empty.setVisibility(View.GONE);
                            recyclerView.setVisibility(View.VISIBLE);
                        } else {
                            empty.setVisibility(View.VISIBLE);
                            recyclerView.setVisibility(View.GONE);
                        }
                    }
                });
            }
        }
    }

    public void onResume() {
        super.onResume();
        if (ignores == null && cid > 0) {
            ignores = ServersList.getInstance().getServer(cid).raw_ignores;
            adapter = new IgnoresAdapter();
            recyclerView.setAdapter(adapter);
            if(adapter.getItemCount() > 0) {
                empty.setVisibility(View.GONE);
                recyclerView.setVisibility(View.VISIBLE);
            } else {
                empty.setVisibility(View.VISIBLE);
                recyclerView.setVisibility(View.GONE);
            }
        }
    }

    public void onIRCEvent(int what, Object obj) {
        switch (what) {
            case NetworkConnection.EVENT_MAKESERVER:
                Server s = (Server) obj;
                if (s.getCid() == cid && getActivity() != null) {
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            ignores = ServersList.getInstance().getServer(cid).raw_ignores;
                            adapter.notifyDataSetChanged();
                            if(adapter.getItemCount() > 0) {
                                empty.setVisibility(View.GONE);
                                recyclerView.setVisibility(View.VISIBLE);
                            } else {
                                empty.setVisibility(View.VISIBLE);
                                recyclerView.setVisibility(View.GONE);
                            }
                        }
                    });
                }
                break;
            case NetworkConnection.EVENT_SETIGNORES:
                IRCCloudJSONObject o = (IRCCloudJSONObject) obj;
                if (o.cid() == cid && getActivity() != null) {
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            ignores = ServersList.getInstance().getServer(cid).raw_ignores;
                            adapter.notifyDataSetChanged();
                            if(adapter.getItemCount() > 0) {
                                empty.setVisibility(View.GONE);
                                recyclerView.setVisibility(View.VISIBLE);
                            } else {
                                empty.setVisibility(View.VISIBLE);
                                recyclerView.setVisibility(View.GONE);
                            }
                        }
                    });
                }
                break;
            default:
                break;
        }
    }
} public class LinksListFragment extends DialogFragment {
    JsonNode event;
    LinksAdapter adapter;
    RecyclerView recyclerView;
    ObjectMapper mapper = new ObjectMapper();

    private class ViewHolder extends RecyclerView.ViewHolder {
        public RowHostmaskBinding binding;

        public ViewHolder(View v) {
            super(v);
            binding = DataBindingUtil.bind(v);
        }
    }

    private class LinksAdapter extends RecyclerView.Adapter<ViewHolder> {
        @Override
        public int getItemCount() {
            return event.get("links").size();
        }

        @Override
        public long getItemId(int pos) {
            return pos;
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = RowHostmaskBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false).getRoot();
            return new ViewHolder(v);
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, final int position) {
            JsonNode link = event.get("links").get(position);
            RowHostmaskBinding row = holder.binding;
            row.setLabel(link.get("server").asText() + "\n" + link.get("info").asText() + "\nHops: " + link.get("hopcount").asText());
            row.removeBtn.setVisibility(View.GONE);
            row.executePendingBindings();
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Context ctx = getActivity();
        if(ctx == null)
            return null;

        if (savedInstanceState != null && savedInstanceState.containsKey("event")) {
            try {
                event = mapper.readValue(savedInstanceState.getString("event"), JsonNode.class);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(R.layout.recyclerview, null);
        recyclerView = v.findViewById(R.id.recycler);
        recyclerView.setLayoutManager(new LinearLayoutManager(v.getContext()));
        recyclerView.setVisibility(View.VISIBLE);
        adapter = new LinksAdapter();
        recyclerView.setAdapter(adapter);
        v.findViewById(android.R.id.empty).setVisibility(View.GONE);
        Dialog d = new AlertDialog.Builder(ctx)
                .setTitle("Servers linked to " + event.get("server").asText())
                .setView(v)
                .setNegativeButton("Close", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                })
                .create();
        d.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
        return d;
    }

    @Override
    public void onSaveInstanceState(Bundle state) {
        state.putString("event", event.toString());
    }

    @Override
    public void setArguments(Bundle args) {
        if(args != null) {
            try {
                event = mapper.readValue(args.getString("event"), JsonNode.class);
            } catch (IOException e) {
                e.printStackTrace();
            }
            if (getActivity() != null && event != null && recyclerView != null) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        adapter = new LinksAdapter();
                        recyclerView.setAdapter(adapter);
                    }
                });
            }
        }
    }
} public class MessageViewFragment extends ListFragment implements NetworkConnection.IRCEventHandler {
    private NetworkConnection conn;
    private TextView statusView;
    private View headerViewContainer;
    private View headerView;
    private View footerViewContainer;
    private TextView backlogFailed;
    private Button loadBacklogButton;
    private TextView unreadTopLabel;
    private TextView unreadBottomLabel;
    private View unreadTopView;
    private View unreadBottomView;
    private TextView highlightsTopLabel;
    private TextView highlightsBottomLabel;
    public ImageView avatar;
    private AvatarsList mAvatarsList = AvatarsList.getInstance();
    private OffsetLinearLayout avatarContainer;
    public Buffer buffer;
    private Server server;
    private long earliest_eid;
    private long backlog_eid = 0;
    private boolean requestingBacklog = false;
    private float avgInsertTime = 0;
    private int newMsgs = 0;
    private long newMsgTime = 0;
    private int newHighlights = 0;
    private MessageViewListener mListener;
    private View awayView = null;
    private TextView awayTxt = null;
    private int timestamp_width = -1;
    private float textSize = 14.0f;
    private View globalMsgView = null;
    private TextView globalMsg = null;
    private ProgressBar spinner = null;
    private final Handler mHandler = new Handler();
    private ColorScheme colorScheme = ColorScheme.getInstance();
    private Typeface hackRegular;
    private HashMap<String, JsonNode> filePropsCache = new HashMap<>();
    private HashSet<String> hiddenFileIDs = new HashSet<>();
    private HashMap<String, Event> msgids = new HashMap<>();
    private String msgid;
    private String buffer_usermask;
    private PrecomputedTextCompat.Params precomputedTextParams = null;
    private boolean fetch_if_needed = false;
    private HashMap<Long,LinkifyTask> linkifyTasks = new HashMap<>();
    private FormatTask mainFormatTask;

    public static final int ROW_MESSAGE = 0;
    public static final int ROW_TIMESTAMP = 1;
    public static final int ROW_BACKLOGMARKER = 2;
    public static final int ROW_SOCKETCLOSED = 3;
    public static final int ROW_LASTSEENEID = 4;
    public static final int ROW_THUMBNAIL = 5;
    public static final int ROW_FILE = 6;
    private static final String TYPE_TIMESTAMP = "__timestamp__";
    private static final String TYPE_BACKLOGMARKER = "__backlog__";
    private static final String TYPE_LASTSEENEID = "__lastseeneid__";

    private MessageAdapter adapter;

    private long currentCollapsedEid = -1;
    private long lastCollapsedEid = -1;
    private CollapsedEventsList collapsedEvents = new CollapsedEventsList();
    private int lastCollapsedDay = -1;
    private HashSet<Long> expandedSectionEids = new HashSet<Long>();
    private RefreshTask refreshTask = null;
    private HeartbeatTask heartbeatTask = null;
    private Ignore ignore = new Ignore();
    private static Timer tapTimer = null;
    private TimerTask tapTimerTask = null;
    public boolean longPressOverride = false;
    private LinkMovementMethodNoLongPress linkMovementMethodNoLongPress = new LinkMovementMethodNoLongPress();
    public boolean ready = false;
    private final Object adapterLock = new Object();
    private final Object formatLock = new Object();

    public View suggestionsContainer = null;
    public GridView suggestions = null;

    private boolean pref_24hr = false;
    private boolean pref_seconds = false;
    private boolean pref_trackUnread = true;
    private boolean pref_timeLeft = false;
    private boolean pref_nickColors = false;
    private boolean pref_hideJoinPart = false;
    private boolean pref_expandJoinPart = false;
    private boolean pref_avatarsOff = false;
    private boolean pref_chatOneLine = false;
    private boolean pref_norealname = false;
    private boolean pref_compact = false;
    private boolean pref_disableLargeEmoji = false;
    private boolean pref_disableInlineFiles = false;
    private boolean pref_disableCodeSpan = false;
    private boolean pref_disableCodeBlock = false;
    private boolean pref_disableQuote = false;
    private boolean pref_inlineImages = false;
    private boolean pref_avatarImages = false;
    private boolean pref_replyCollapse = false;
    private boolean pref_mentionColors = false;
    private boolean pref_muted = false;
    private boolean pref_noColor = false;

    private static Pattern IS_CODE_BLOCK = Pattern.compile("```([\\s\\S]+?)```(?=(?!`)[\\W\\s\\n]|$)");

    private class LinkMovementMethodNoLongPress extends IRCCloudLinkMovementMethod {
        @Override
        public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
            int action = event.getAction();

            if (!longPressOverride && (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN)) {
                return super.onTouchEvent(widget, buffer, event);
            }
            return false;
        }
    }

    private class MessageAdapter extends BaseAdapter {
        final ArrayList<Event> data;
        private ListFragment ctx;
        private long max_eid = 0;
        private long min_eid = 0;
        private int lastDay = -1;
        private int lastSeenEidMarkerPosition = -1;
        private int currentGroupPosition = -1;
        private TreeSet<Integer> unseenHighlightPositions;

        private class ViewHolder {
            int type;
            LinearLayout lastSeenEidWrapper;
            LinearLayout messageContainer;
            LinearLayout socketClosedBar;
            LinearLayout thumbnailWrapper;
            LinearLayout replyCountContainer;
            TextView timestamp;
            TextView timestamp_left;
            TextView timestamp_right;
            TextView message;
            TextView expandable;
            TextView nickname;
            TextView realname;
            TextView filename;
            TextView metadata;
            TextView extension;
            TextView reply;
            TextView replyCountIcon;
            TextView replyCount;
            ImageView failed;
            ImageView avatar;
            GifImageView thumbnail;
            ProgressBar progress;
            View quoteBorder;
        }

        public MessageAdapter(ListFragment context, int capacity) {
            ctx = context;
            data = new ArrayList<>(capacity + 10);
            unseenHighlightPositions = new TreeSet<>(Collections.reverseOrder());
        }

        public void clear() {
            max_eid = 0;
            min_eid = 0;
            lastDay = -1;
            lastSeenEidMarkerPosition = -1;
            currentGroupPosition = -1;
            synchronized (data) {
                data.clear();
            }
            unseenHighlightPositions.clear();
            avatarContainer.setVisibility(View.GONE);
            avatar.setTag(null);
        }

        public void clearPending() {
            synchronized (data) {
                for (int i = 0; i < data.size(); i++) {
                    if (data.get(i).pending) {
                        data.remove(i);
                        i--;
                    }
                }
            }
        }

        public void removeItem(long eid) {
            synchronized (data) {
                for (int i = 0; i < data.size(); i++) {
                    if (data.get(i).eid == eid) {
                        data.remove(i);
                        i--;
                    }
                }
            }
        }

        public int getBacklogMarkerPosition() {
            try {
                synchronized (data) {
                    for (int i = 0; i < data.size(); i++) {
                        Event e = data.get(i);
                        if (e != null && e.row_type == ROW_BACKLOGMARKER) {
                            return i;
                        }
                    }
                }
            } catch (Exception e) {
            }
            return -1;
        }

        public int insertLastSeenEIDMarker() {
            if (buffer == null)
                return -1;

            synchronized (data) {
                if ((min_eid > 0 && buffer.getLast_seen_eid() > 0 && min_eid >= buffer.getLast_seen_eid()) || (data.size() == 0 && buffer.getLast_seen_eid() > 0)) {
                    lastSeenEidMarkerPosition = 0;
                } else {
                    for (int i = data.size() - 1; i >= 0; i--) {
                        if (data.get(i).eid <= buffer.getLast_seen_eid() && data.get(i).row_type != ROW_LASTSEENEID) {
                            lastSeenEidMarkerPosition = i;
                            break;
                        }
                    }
                    if (lastSeenEidMarkerPosition > 0 && lastSeenEidMarkerPosition != data.size() - 1 && !data.get(lastSeenEidMarkerPosition).self && !data.get(lastSeenEidMarkerPosition).pending) {
                        if (data.get(lastSeenEidMarkerPosition - 1).row_type == ROW_TIMESTAMP)
                            lastSeenEidMarkerPosition--;
                        if (lastSeenEidMarkerPosition > 0) {
                            Event e = new Event();
                            e.bid = buffer.getBid();
                            e.cid = buffer.getCid();
                            e.eid = buffer.getLast_seen_eid() + 1;
                            e.type = TYPE_LASTSEENEID;
                            e.row_type = ROW_LASTSEENEID;
                            if(EventsList.getInstance().getEvent(buffer.getLast_seen_eid(), buffer.getBid()) != null)
                                e.from = EventsList.getInstance().getEvent(buffer.getLast_seen_eid(), buffer.getBid()).from;
                            e.bg_color = colorScheme.socketclosedBackgroundDrawable;
                            addItem(e.eid, e);
                            EventsList.getInstance().addEvent(e);
                            for (int i = 0; i < data.size(); i++) {
                                if (data.get(i).row_type == ROW_LASTSEENEID && data.get(i) != e) {
                                    EventsList.getInstance().deleteEvent(data.get(i).eid, buffer.getBid());
                                    data.remove(i);
                                }
                            }
                        }
                    } else {
                        lastSeenEidMarkerPosition = -1;
                    }
                }
                if (lastSeenEidMarkerPosition > 0 && lastSeenEidMarkerPosition <= currentGroupPosition)
                    currentGroupPosition++;

                if (lastSeenEidMarkerPosition == -1) {
                    for (int i = data.size() - 1; i >= 0; i--) {
                        if (data.get(i).row_type == ROW_LASTSEENEID) {
                            lastSeenEidMarkerPosition = i;
                            break;
                        }
                    }
                }
            }
            return lastSeenEidMarkerPosition;
        }

        public void clearLastSeenEIDMarker() {
            if(buffer != null) {
                synchronized (data) {
                    for (int i = 0; i < data.size(); i++) {
                        if (data.get(i).row_type == ROW_LASTSEENEID) {
                            EventsList.getInstance().deleteEvent(data.get(i).eid, buffer.getBid());
                            data.remove(i);
                        }
                    }
                }
                if (lastSeenEidMarkerPosition > 0)
                    lastSeenEidMarkerPosition = -1;
            }
        }

        public int getLastSeenEIDPosition() {
            return lastSeenEidMarkerPosition;
        }

        public int getUnreadHighlightsAbovePosition(int pos) {
            int count = 0;

            Iterator<Integer> i = unseenHighlightPositions.iterator();
            while (i.hasNext()) {
                Integer p = i.next();
                if (p < pos)
                    break;
                count++;
            }

            return unseenHighlightPositions.size() - count;
        }

        public void addItem(long eid, Event e) {
            synchronized (data) {
                Calendar calendar = Calendar.getInstance();
                calendar.setTimeInMillis(e.getTime());
                int insert_pos = -1;
                SimpleDateFormat formatter = null;
                if (e.timestamp == null || e.timestamp.length() == 0) {
                    formatter = new SimpleDateFormat("h:mm a", Locale.US);
                    if (pref_24hr) {
                        if (pref_seconds)
                            formatter = new SimpleDateFormat("HH:mm:ss", Locale.US);
                        else
                            formatter = new SimpleDateFormat("HH:mm", Locale.US);
                    } else if (pref_seconds) {
                        formatter = new SimpleDateFormat("h:mm:ss a", Locale.US);
                    }
                    e.timestamp = formatter.format(calendar.getTime());
                }
                e.group_eid = currentCollapsedEid;
                if (e.group_msg != null && e.html == null)
                    e.html = e.group_msg;

                e.ready_for_display = true;

                if (e.day < 1) {
                    e.day = calendar.get(Calendar.DAY_OF_YEAR);
                }

                if (currentGroupPosition > 0 && currentGroupPosition < data.size() && eid == currentCollapsedEid && e.eid != eid) { //Shortcut for replacing the current group
                    calendar.setTimeInMillis(e.getTime());
                    lastDay = e.day;
                    data.remove(currentGroupPosition);
                    data.add(currentGroupPosition, e);
                    insert_pos = currentGroupPosition;
                } else if (eid > max_eid || data.size() == 0 || eid > data.get(data.size() - 1).eid) { //Message at the bottom
                    if (data.size() > 0) {
                        lastDay = data.get(data.size() - 1).day;
                    } else {
                        lastDay = 0;
                    }
                    max_eid = eid;
                    data.add(e);
                    insert_pos = data.size() - 1;
                } else if (min_eid > eid) { //Message goes on top
                    if (data.size() > 1) {
                        lastDay = data.get(1).day;
                        if (calendar.get(Calendar.DAY_OF_YEAR) != lastDay) { //Insert above the dateline
                            data.add(0, e);
                            insert_pos = 0;
                        } else { //Insert below the dateline
                            data.add(1, e);
                            insert_pos = 1;
                        }
                    } else {
                        data.add(0, e);
                        insert_pos = 0;
                    }
                } else {
                    int i = 0;
                    for (Event e1 : data) {
                        if (e1.row_type != ROW_TIMESTAMP && e1.eid > eid && e.eid == eid && e1.group_eid != eid) { //Insert the message
                            if (i > 0 && data.get(i - 1).row_type != ROW_TIMESTAMP) {
                                lastDay = data.get(i - 1).day;
                                data.add(i, e);
                                insert_pos = i;
                                break;
                            } else { //There was a date line above our insertion point
                                lastDay = e1.day;
                                if (calendar.get(Calendar.DAY_OF_YEAR) != lastDay) { //Insert above the dateline
                                    if (i > 1) {
                                        lastDay = data.get(i - 2).day;
                                    } else {
                                        //We're above the first dateline, so we'll need to put a new one on top!
                                        lastDay = 0;
                                    }
                                    data.add(i - 1, e);
                                    insert_pos = i - 1;
                                } else { //Insert below the dateline
                                    data.add(i, e);
                                    insert_pos = i;
                                }
                                break;
                            }
                        } else if (e1.row_type != ROW_TIMESTAMP && (e1.eid == eid || e1.group_eid == eid)) { //Replace the message
                            lastDay = calendar.get(Calendar.DAY_OF_YEAR);
                            data.remove(i);
                            data.add(i, e);
                            insert_pos = i;
                            break;
                        }
                        i++;
                    }
                }

                if (insert_pos == -1) {
                    Log.e("IRCCloud", "Couldn't insert EID: " + eid + " MSG: " + e.html);
                    data.add(e);
                    insert_pos = data.size() - 1;
                }

                if (eid > buffer.getLast_seen_eid() && e.highlight && !pref_muted)
                    unseenHighlightPositions.add(insert_pos);

                if (eid < min_eid || min_eid == 0)
                    min_eid = eid;

                if (eid == currentCollapsedEid && e.eid == eid) {
                    currentGroupPosition = insert_pos;
                } else if (currentCollapsedEid == -1) {
                    currentGroupPosition = -1;
                }

                if (calendar.get(Calendar.DAY_OF_YEAR) != lastDay) {
                    if (formatter == null)
                        formatter = new SimpleDateFormat("EEEE, MMMM dd, yyyy", Locale.US);
                    else
                        formatter.applyPattern("EEEE, MMMM dd, yyyy");
                    Event d = new Event();
                    d.type = TYPE_TIMESTAMP;
                    d.row_type = ROW_TIMESTAMP;
                    d.eid = eid - 1;
                    d.timestamp = formatter.format(calendar.getTime());
                    d.bg_color = colorScheme.timestampBackgroundDrawable;
                    d.day = lastDay = calendar.get(Calendar.DAY_OF_YEAR);
                    data.add(insert_pos++, d);
                    if (currentGroupPosition > -1)
                        currentGroupPosition++;
                }

                String avatar = e.getAvatarURL(72);
                if (insert_pos > 0) {
                    Event prev = data.get(insert_pos - ((e.row_type == ROW_LASTSEENEID)?2:1));
                    String prevAvatar = prev.getAvatarURL(72);
                    e.header = (e.isMessage() && e.from != null && e.from.length() > 0 && e.group_eid < 1) && (prev.from == null || !prev.from.equals(e.from) || !prev.type.equals(e.type) || (avatar == null && prevAvatar != null) || (avatar != null && prevAvatar == null) || (avatar != null && prevAvatar != null && !avatar.equals(prevAvatar)));
                }

                if (insert_pos < (data.size() - 1)) {
                    Event next = data.get(insert_pos + 1);
                    if (!e.isMessage() && e.row_type != ROW_LASTSEENEID) {
                        next.header = (next.isMessage() && next.from != null && next.from.length() > 0 && next.group_eid < 1);
                    }
                    if (next.from != null && next.from.equals(e.from) && next.type.equals(e.type)) {
                        String nextAvatar = next.getAvatarURL(72);
                        next.header = false;//!avatar.equals(nextAvatar);
                    }
                }
            }
        }

        public void insertAbove(long eid, Event e, boolean backlog) {
            synchronized (data) {
                if (e.day < 1) {
                    Calendar calendar = Calendar.getInstance();
                    calendar.setTimeInMillis(e.getTime());
                    e.day = calendar.get(Calendar.DAY_OF_YEAR);
                }
                for (int i = 0; i < data.size(); i++) {
                    if(data.get(i).eid == eid) {
                        e.ready_for_display = true;
                        if(!backlog)
                            format(e);
                        data.add(i, e);
                        break;
                    }
                }
            }
        }

        public void insertBelow(long eid, Event e, boolean backlog) {
            synchronized (data) {
                if (e.day < 1) {
                    Calendar calendar = Calendar.getInstance();
                    calendar.setTimeInMillis(e.getTime());
                    e.day = calendar.get(Calendar.DAY_OF_YEAR);
                }
                if(data.size() == 0 || data.get(data.size() - 1).eid == eid) {
                    e.ready_for_display = true;
                    if(!backlog)
                        format(e);
                    data.add(e);
                    return;
                }

                for (int i = 0; i < data.size(); i++) {
                    if(data.get(i).eid == eid) {
                        e.ready_for_display = true;
                        if(!backlog)
                            format(e);
                        data.add(i+1, e);
                        break;
                    }
                }
            }
        }

        @Override
        public int getCount() {
            synchronized (data) {
                if (ctx != null)
                    return data.size();
                else
                    return 0;
            }
        }

        @Override
        public Object getItem(int position) {
            synchronized (data) {
                if (position < data.size())
                    return data.get(position);
                else
                    return null;
            }
        }

        @Override
        public long getItemId(int position) {
            synchronized (data) {
                if (position < data.size())
                    return data.get(position).eid;
                else
                    return -1;
            }
        }

        public void format(Event e) {
            format(e, false);
        }

        public void format(Event e, boolean skipLinkify) {
            if (e != null && e.ready_for_display) {
                synchronized (formatLock) {
                    if (e.formatted_nick == null && e.from != null && e.from.length() > 0) {
                        e.formatted_nick = ColorFormatter.html_to_spanned("<b>" + ColorFormatter.irc_to_html(collapsedEvents.formatNick(e.from_nick, e.from, e.from_mode, !e.self && pref_nickColors, ColorScheme.getInstance().selfTextColor)) + "</b>", false, null);
                    }
                    if (e.formatted_realname == null && e.from_realname != null && e.from_realname.length() > 0) {
                        e.formatted_realname = ColorFormatter.html_to_spanned(ColorFormatter.irc_to_html(TextUtils.htmlEncode(ColorFormatter.emojify(e.from_realname))), true, null);
                        e.linkified = false;
                    }
                    if (e.html != null && e.formatted == null) {
                        try {
                            String html = e.html;
                            html = ColorFormatter.emojify(ColorFormatter.irc_to_html(html, (e.entities != null && e.entities.has("mentions"))?e.entities.get("mentions"):null, e.mention_offset, (e.entities != null && e.entities.has("mention_data"))?e.entities.get("mention_data"):null,server!=null?server.getCid():0, pref_noColor));
                            if(e.edited)
                                html += " <font color=\"#" + Integer.toHexString(ColorScheme.getInstance().timestampColor).substring(2) + "\">(edited)</font>";
                            e.formatted = ColorFormatter.html_to_spanned(html, e.linkify, (e.row_type == ROW_THUMBNAIL) ? null : server, e.entities, pref_mentionColors);
                            if(e.formatted != null && !pref_disableQuote && e.type.equals("buffer_msg") && ColorFormatter.is_blockquote(e.formatted.toString())) {
                                e.formatted = (Spanned)e.formatted.subSequence(1, e.formatted.length());
                                e.quoted = true;
                            } else {
                                e.quoted = false;
                            }
                            if(e.html_prefix != null && e.html_prefix.length() > 0) {
                                SpannableStringBuilder sb = new SpannableStringBuilder();
                                sb.append(ColorFormatter.html_to_spanned(ColorFormatter.irc_to_html(e.html_prefix)));
                                if(!e.html_prefix.endsWith(" "))
                                    sb.append(" ");
                                sb.append(e.formatted);
                                e.formatted = sb;
                            }
                            if (e.group_msg == null && e.msg != null && e.msg.length() > 0) {
                                e.contentDescription = ColorFormatter.html_to_spanned(ColorFormatter.irc_to_html(e.msg), false, server);
                            }
                        } catch (Exception ex) {
                            ex.printStackTrace();
                        }
                        e.linkified = false;
                    }
                }

                if(e.linkify && !e.linkified && !skipLinkify) {
                    if(linkifyTasks.containsKey(e.eid)) {
                        linkifyTasks.get(e.eid).cancel(true);
                        linkifyTasks.remove(e.eid);
                    }
                    LinkifyTask t = new LinkifyTask(e);
                    linkifyTasks.put(e.eid, t);
                    t.execute((Void)null);
                } else if (precomputedTextParams != null && e.formatted != null) {
                    try {
                        e.formatted = PrecomputedTextCompat.create(e.formatted, precomputedTextParams);
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }

        @Override
        public int getItemViewType(int position) {
            synchronized (data) {
                return data.get(position).row_type;
            }
        }

        private Runnable refreshSoonRunnable = new Runnable() {
            @Override
            public void run() {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        notifyDataSetChanged();
                    }
                });
            }
        };

        private void refreshSoon() {
            mHandler.removeCallbacks(refreshSoonRunnable);
            mHandler.postDelayed(refreshSoonRunnable, 100);
        }

        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            if (position >= data.size() || ctx == null)
                return null;

            final Event e;
            synchronized (data) {
                e = data.get(position);
            }
            View row = convertView;
            ViewHolder holder;

            if (row != null && ((ViewHolder) row.getTag()).type != e.row_type)
                row = null;

            if (row == null) {
                LayoutInflater inflater = ctx.getLayoutInflater(null);
                if (e.row_type == ROW_BACKLOGMARKER)
                    row = inflater.inflate(R.layout.row_backlogmarker, parent, false);
                else if (e.row_type == ROW_TIMESTAMP)
                    row = inflater.inflate(R.layout.row_timestamp, parent, false);
                else if (e.row_type == ROW_SOCKETCLOSED)
                    row = inflater.inflate(R.layout.row_socketclosed, parent, false);
                else if (e.row_type == ROW_LASTSEENEID)
                    row = inflater.inflate(R.layout.row_lastseeneid, parent, false);
                else if (e.row_type == ROW_THUMBNAIL || e.row_type == ROW_FILE)
                    row = inflater.inflate(R.layout.row_thumbnail, parent, false);
                else
                    row = inflater.inflate(R.layout.row_message, parent, false);

                holder = new ViewHolder();
                holder.timestamp = row.findViewById(R.id.timestamp);
                holder.timestamp_left = row.findViewById(R.id.timestamp_left);
                holder.timestamp_right = row.findViewById(R.id.timestamp_right);
                holder.message = row.findViewById(R.id.message);
                holder.expandable = row.findViewById(R.id.expandable);
                if(holder.expandable != null)
                    holder.expandable.setTypeface(FontAwesome.getTypeface());
                holder.nickname = row.findViewById(R.id.nickname);
                holder.realname = row.findViewById(R.id.realname);
                holder.failed = row.findViewById(R.id.failed);
                holder.avatar = row.findViewById(R.id.avatar);
                holder.lastSeenEidWrapper = row.findViewById(R.id.lastSeenEidWrapper);
                holder.messageContainer = row.findViewById(R.id.messageContainer);
                holder.socketClosedBar = row.findViewById(R.id.socketClosedBar);
                holder.thumbnailWrapper = row.findViewById(R.id.thumbnailWrapper);
                holder.thumbnail = row.findViewById(R.id.thumbnail);
                holder.filename = row.findViewById(R.id.filename);
                holder.metadata = row.findViewById(R.id.metadata);
                holder.extension = row.findViewById(R.id.extension);
                holder.progress = row.findViewById(R.id.progress);
                holder.quoteBorder = row.findViewById(R.id.quoteBorder);
                holder.replyCountContainer = row.findViewById(R.id.replyCountContainer);
                holder.reply = row.findViewById(R.id.reply);
                if(holder.reply != null)
                    holder.reply.setTypeface(FontAwesome.getTypeface());
                holder.replyCountIcon = row.findViewById(R.id.replyCountIcon);
                if(holder.replyCountIcon != null) {
                    holder.replyCountIcon.setTypeface(FontAwesome.getTypeface());
                }
                holder.replyCount = row.findViewById(R.id.replyCount);
                holder.type = e.row_type;

                row.setTag(holder);
            } else {
                holder = (ViewHolder) row.getTag();
            }

            row.setOnClickListener(new OnItemClickListener(e));
            row.setOnLongClickListener(new OnItemLongClickListener(e));
            row.setContentDescription(e.contentDescription);

            if ((e.html != null && e.formatted == null) || (e.formatted_nick == null && e.from != null && e.from.length() > 0) || (e.formatted_realname == null && e.from_realname != null && e.from_realname.length() > 0)) {
                adapter.format(e, true);
            }

            if (e.row_type == ROW_MESSAGE) {
                if (e.bg_color == colorScheme.contentBackgroundColor)
                    row.setBackgroundDrawable(null);
                else
                    row.setBackgroundColor(e.bg_color);
                if (e.contentDescription != null && e.from != null && e.from.length() > 0 && e.msg != null && e.msg.length() > 0 && e.group_msg == null) {
                    row.setContentDescription(TextUtils.concat("Message from " + e.from + ": ", e.contentDescription, ", at " + e.timestamp));
                }
            }

            boolean mono = PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("monospace", false);

            if(holder.timestamp_left != null) {
                if(pref_timeLeft && (pref_chatOneLine || pref_avatarsOff)) {
                    holder.timestamp_left.setVisibility(View.VISIBLE);
                    holder.timestamp_right.setVisibility(View.GONE);
                    holder.timestamp = holder.timestamp_left;
                } else {
                    holder.timestamp_left.setVisibility(View.GONE);
                    holder.timestamp_right.setVisibility(View.VISIBLE);
                    holder.timestamp = holder.timestamp_right;
                }
            }

            if (holder.timestamp != null) {
                holder.timestamp.setTypeface(mono ? hackRegular : Typeface.DEFAULT);
                if (e.row_type == ROW_TIMESTAMP) {
                    holder.timestamp.setTextSize(textSize);
                } else {
                    holder.timestamp.setTextSize(textSize - 2);

                    if (timestamp_width == -1) {
                        String s = " 88:88";
                        if (pref_seconds)
                            s += ":88";
                        if (!pref_24hr)
                            s += " MM";
                        timestamp_width = (int) holder.timestamp.getPaint().measureText(s);
                    }
                    holder.timestamp.setMinWidth(timestamp_width);
                }
                if (e.highlight && !e.self)
                    holder.timestamp.setTextColor(colorScheme.highlightTimestampColor);
                else if (e.row_type != ROW_TIMESTAMP)
                    holder.timestamp.setTextColor(colorScheme.timestampColor);
                holder.timestamp.setText(e.timestamp);
                ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams)holder.timestamp.getLayoutParams();
                lp.topMargin = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, pref_compact?0:2, getResources().getDisplayMetrics());

            }
            if (e.row_type == ROW_SOCKETCLOSED) {
                if ((e.msg != null && e.msg.length() > 0) || (e.group_msg != null && e.group_msg.length() > 0)) {
                    holder.timestamp.setVisibility(View.VISIBLE);
                    holder.message.setVisibility(View.VISIBLE);
                } else {
                    holder.timestamp.setVisibility(View.GONE);
                    holder.message.setVisibility(View.GONE);
                }
            }

            if (holder.message != null && e.html != null) {
                holder.message.setMovementMethod(linkMovementMethodNoLongPress);
                holder.message.setOnClickListener(new OnItemClickListener(e));
                holder.message.setOnLongClickListener(new OnItemLongClickListener(e));
                if (mono || (e.msg != null && e.msg.startsWith("<pre>")) || e.code_block) {
                    holder.message.setTypeface(hackRegular);
                } else {
                    holder.message.setTypeface(Typeface.DEFAULT);
                }
                try {
                    holder.message.setTextColor(e.color);
                } catch (Exception e1) {

                }
                if (e.color == colorScheme.timestampColor || e.color == colorScheme.collapsedRowTextColor || e.pending)
                    holder.message.setLinkTextColor(colorScheme.lightLinkColor);
                else
                    holder.message.setLinkTextColor(colorScheme.linkColor);

                if(pref_compact)
                    holder.message.setLineSpacing(0,1);
                else
                    holder.message.setLineSpacing(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()),1);

                Spanned formatted = e.formatted;
                if(formatted != null && !pref_avatarsOff && e.parent_eid == 0 && ((e.from != null && e.from.length() > 0) || e.type.equals("buffer_me_msg")) && e.group_eid < 0 && (pref_chatOneLine || e.type.equals("buffer_me_msg"))) {
                    final int width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, textSize+4, getResources().getDisplayMetrics());
                    Bitmap b = null;
                    URL avatarURL = null;
                    try {
                        if(pref_avatarImages && e.getAvatarURL(width) != null)
                            avatarURL = new URL(e.getAvatarURL(width));
                    } catch (Exception e1) {
                        e1.printStackTrace();
                    }
                    if(avatarURL != null) {
                        try {
                            final String avatarURL_string = avatarURL.toString();
                            b = ImageList.getInstance().getImage(avatarURL);
                            if(b != null && b.getWidth() != width)
                                b = Bitmap.createScaledBitmap(b, width, width, false);
                            ImageList.getInstance().fetchImage(avatarURL, 0, new ImageList.OnImageFetchedListener() {
                                @Override
                                public void onImageFetched(Bitmap image) {
                                    if(image != null) {
                                        refreshSoon();
                                    } else {
                                        String gravatar = e.getGravatar(width);
                                        if(gravatar != null && !gravatar.equals(avatarURL_string)) {
                                            try {
                                                ImageList.getInstance().fetchImage(new URL(gravatar), 0, new ImageList.OnImageFetchedListener() {
                                                    @Override
                                                    public void onImageFetched(Bitmap image) {
                                                        refreshSoon();
                                                    }
                                                }, 600000);
                                            } catch (Exception e) {
                                                e.printStackTrace();
                                            }
                                        }
                                    }
                                }
                            }, 600000);
                        } catch (IOException e1) {
                            e1.printStackTrace();
                        }
                    }
                    boolean hasAvatarImage = (b != null);
                    if(b == null) {
                        b = mAvatarsList.getAvatar(e.cid, e.type.equals("buffer_me_msg")?e.nick:e.from_nick, e.from).getBitmap(ColorScheme.getInstance().isDarkTheme, width, e.self);
                    }
                    if(b != null) {
                        SpannableStringBuilder s = new SpannableStringBuilder(formatted);
                        s.insert(0, "\u202f\u202f");
                        s.setSpan(new ImageSpan(getActivity(), b) {
                            @Override
                            public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint p) {
                                Bitmap bitmap = ((BitmapDrawable)getDrawable()).getBitmap();
                                BitmapShader shader;
                                shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

                                Paint paint = new Paint();
                                paint.setAntiAlias(true);
                                paint.setShader(shader);

                                RectF rect = new RectF(0.0f, 0.0f, bitmap.getWidth(), bitmap.getHeight());

                                canvas.save();

                                canvas.translate(x, y - bitmap.getHeight() + paint.getFontMetricsInt().descent);
                                int radius = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 2, getResources().getDisplayMetrics());
                                canvas.drawRoundRect(rect, radius, radius, paint);
                                canvas.restore();
                            }
                        }, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                        if(avatarURL != null && hasAvatarImage) {
                            final Uri uri = Uri.parse(e.getAvatarURL(Resources.getSystem().getDisplayMetrics().widthPixels));
                            s.setSpan(new ClickableSpan() {
                                @Override
                                public void onClick(View widget) {
                                    IRCCloudLinkMovementMethod.launchURI(uri, getContext());
                                }
                            }, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                        }
                        s.setSpan(new ColorFormatter.TypefaceSpan(ColorFormatter.sourceSansPro), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                        formatted = s;
                    }
                }
                holder.message.setText(formatted);
                holder.message.setTextSize(textSize);
                if(e.code_block) {
                    int padding = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics());
                    holder.message.setPadding(padding, padding, padding, padding);
                    holder.message.setBackgroundColor(ColorScheme.getInstance().codeSpanBackgroundColor);
                } else {
                    holder.message.setPadding(0,0,0,0);
                    holder.message.setBackgroundDrawable(null);
                }
                //precomputedTextParams = new PrecomputedTextCompat.Params.Builder(holder.message.getPaint()).build();
            }

            if (holder.expandable != null) {
                ViewGroup.LayoutParams lp = holder.expandable.getLayoutParams();
                lp.width = lp.height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, textSize, getResources().getDisplayMetrics());
                holder.expandable.setLayoutParams(lp);
                holder.expandable.setTextSize(textSize);
                if (e.group_eid > 0 && (e.group_eid != e.eid || expandedSectionEids.contains(e.group_eid))) {
                    if (expandedSectionEids.contains(e.group_eid)) {
                        if (e.group_eid == e.eid + 1) {
                            holder.expandable.setText(FontAwesome.MINUS_SQUARE_O);
                            holder.expandable.setContentDescription("expanded");
                            row.setBackgroundColor(colorScheme.collapsedHeadingBackgroundColor);
                            row.setContentDescription(TextUtils.concat("Expanded events heading: ", e.contentDescription));
                        } else {
                            holder.expandable.setText(FontAwesome.ANGLE_RIGHT);
                            holder.expandable.setContentDescription("collapse");
                            row.setBackgroundColor(colorScheme.contentBackgroundColor);
                        }
                    } else {
                        holder.expandable.setText(FontAwesome.PLUS_SQUARE_O);
                        holder.expandable.setContentDescription("expand");
                        row.setContentDescription(TextUtils.concat("Collapsed events: ", e.contentDescription));
                    }
                    holder.expandable.setVisibility(View.VISIBLE);
                } else {
                    holder.expandable.setVisibility(View.GONE);
                }
                holder.expandable.setTextColor(colorScheme.expandCollapseIndicatorColor);
            }

            if (holder.failed != null)
                holder.failed.setVisibility(e.failed ? View.VISIBLE : View.GONE);

            if (holder.messageContainer != null) {
                if(pref_compact)
                    holder.messageContainer.setPadding(0,0,0,0);
                else
                    holder.messageContainer.setPadding(0,(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()),0,(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()));
            }

            if (holder.avatar != null) {
                ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams)holder.avatar.getLayoutParams();
                if(pref_avatarsOff || pref_chatOneLine || (e.row_type == ROW_SOCKETCLOSED && e.group_msg == null && (e.msg == null || e.msg.length() == 0))) {
                    holder.avatar.setImageBitmap(null);
                    lp.topMargin = lp.width = lp.height = 0;
                } else {
                    lp.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6, getResources().getDisplayMetrics());
                    lp.width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 32, getResources().getDisplayMetrics());
                    Bitmap b = null;
                    if (e.group_eid < 1 && e.from != null && e.from.length() > 0 && (pref_chatOneLine || e.header)) {
                        URL avatarURL = null;
                        try {
                            if(pref_avatarImages && e.getAvatarURL(lp.width) != null)
                                avatarURL = new URL(e.getAvatarURL(lp.width));
                        } catch (Exception e1) {
                            e1.printStackTrace();
                        }
                        if(avatarURL != null) {
                            final int width = lp.width;
                            final String avatarURL_string = avatarURL.toString();
                            try {
                                b = ImageList.getInstance().getImage(avatarURL, width);
                                ImageList.getInstance().fetchImage(avatarURL, width, new ImageList.OnImageFetchedListener() {
                                    @Override
                                    public void onImageFetched(Bitmap image) {
                                        if(image != null) {
                                            refreshSoon();
                                        } else {
                                            String gravatar = e.getGravatar(width);
                                            if(gravatar != null && !gravatar.equals(avatarURL_string)) {
                                                try {
                                                    ImageList.getInstance().fetchImage(new URL(gravatar), 0, new ImageList.OnImageFetchedListener() {
                                                        @Override
                                                        public void onImageFetched(Bitmap image) {
                                                            refreshSoon();
                                                        }
                                                    }, 600000);
                                                } catch (Exception e) {
                                                    e.printStackTrace();
                                                }
                                            }
                                        }
                                    }
                                }, 600000);
                            } catch (IOException e1) {
                            }
                        }
                        if(b == null) {
                            Avatar a = mAvatarsList.getAvatar(e.cid, e.from_nick, e.from);
                            b = a.getBitmap(ColorScheme.getInstance().isDarkTheme, lp.width, e.self);
                            holder.avatar.setTag(a);
                            holder.avatar.setOnClickListener(null);
                        } else {
                            final Uri uri = Uri.parse(e.getAvatarURL(Resources.getSystem().getDisplayMetrics().widthPixels));
                             holder.avatar.setOnClickListener(new OnClickListener() {
                                @Override
                                public void onClick(View v) {
                                    IRCCloudLinkMovementMethod.launchURI(uri, getContext());
                                }
                            });
                        }
                        holder.avatar.setVisibility(View.VISIBLE);
                    }
                    holder.avatar.setImageBitmap(b);
                    if(b != null)
                        lp.height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16 * ((!e.header || e.group_eid > 0)?1:2), getResources().getDisplayMetrics());
                    else
                        lp.height = 0;
                }
                holder.avatar.setLayoutParams(lp);
            }

            if(!pref_chatOneLine && e.header && e.formatted_nick != null && e.formatted_nick.length() > 0 && e.group_eid < 1) {
                if (holder.nickname != null) {
                    holder.nickname.setVisibility(View.VISIBLE);
                    if (mono)
                        holder.nickname.setTypeface(hackRegular);
                    else
                        holder.nickname.setTypeface(Typeface.DEFAULT);
                    holder.nickname.setTextSize(textSize);
                    holder.nickname.setText(e.formatted_nick);

                    ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) holder.nickname.getLayoutParams();
                    lp.leftMargin = (pref_timeLeft&&pref_avatarsOff)?timestamp_width:0;
                    holder.nickname.setLayoutParams(lp);
                }

                if (holder.realname != null) {
                    holder.realname.setMovementMethod(linkMovementMethodNoLongPress);
                    holder.realname.setVisibility((pref_chatOneLine || pref_norealname) ? View.GONE : View.VISIBLE);
                    if (mono)
                        holder.realname.setTypeface(hackRegular);
                    else
                        holder.realname.setTypeface(Typeface.DEFAULT);
                    holder.realname.setTextSize(textSize);
                    holder.realname.setTextColor(colorScheme.timestampColor);
                    holder.realname.setText(e.formatted_realname);
                }
            } else {
                if (holder.nickname != null)
                    holder.nickname.setVisibility(View.GONE);
                if (holder.realname != null)
                    holder.realname.setVisibility(View.GONE);
                if (holder.avatar != null)
                    holder.avatar.setVisibility((pref_avatarsOff || pref_chatOneLine) ? View.GONE : View.VISIBLE);
                if(holder.message != null) {
                    ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) holder.message.getLayoutParams();
                    lp.leftMargin = 0;
                    holder.message.setLayoutParams(lp);
                }
            }

            if(holder.lastSeenEidWrapper != null) {
                ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) holder.lastSeenEidWrapper.getLayoutParams();
                if(!pref_avatarsOff && !pref_chatOneLine)
                    lp.leftMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 48, getResources().getDisplayMetrics());
                else
                    lp.leftMargin = 0;
                holder.lastSeenEidWrapper.setLayoutParams(lp);
                holder.lastSeenEidWrapper.setMinimumHeight((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, pref_compact?(textSize + 2):(textSize + 4), getResources().getDisplayMetrics()));
            }

            if(holder.socketClosedBar != null) {
                holder.socketClosedBar.setMinimumHeight((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, textSize + 2, getResources().getDisplayMetrics()));
            }

            if(e.row_type == ROW_BACKLOGMARKER)
                row.setMinimumHeight((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, pref_compact?4:(textSize + 2), getResources().getDisplayMetrics()));

            if(holder.quoteBorder != null)
                holder.quoteBorder.setVisibility(e.quoted ? View.VISIBLE : View.GONE );

            if(holder.reply != null) {
                LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(holder.reply.getLayoutParams());
                if(!pref_replyCollapse && (e.is_reply || e.reply_count > 0)) {
                    if(!pref_chatOneLine && e.header) {
                        lp.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 2, getResources().getDisplayMetrics());
                    } else {
                        lp.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, -4, getResources().getDisplayMetrics());;
                    }
                    holder.reply.setLayoutParams(lp);
                    holder.reply.setText(e.is_reply ? FontAwesome.COMMENTS : FontAwesome.COMMENT);
                    holder.reply.setTextColor(0x66000000 + Integer.parseInt(ColorScheme.colorForNick(e.is_reply ? e.reply() : e.msgid, ColorScheme.getInstance().isDarkTheme), 16));
                    holder.reply.setVisibility(View.VISIBLE);
                    holder.reply.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            if(getActivity() != null && getActivity() instanceof MainActivity) {
                                ((MainActivity)getActivity()).setBuffer(buffer.getBid(), e.is_reply ? e.reply() : e.msgid);
                            }
                        }
                    });
                } else {
                    lp.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 2, getResources().getDisplayMetrics());
                    holder.reply.setLayoutParams(lp);
                    holder.reply.setVisibility(View.INVISIBLE);
                    holder.reply.setOnClickListener(null);
                }
            }

            if(holder.replyCountContainer != null) {
                if(e.reply_count > 0 && pref_replyCollapse) {
                    holder.replyCountIcon.setText(FontAwesome.COMMENT);
                    holder.replyCountIcon.setTextColor(0x66000000 + Integer.parseInt(ColorScheme.colorForNick(e.msgid, ColorScheme.getInstance().isDarkTheme), 16));
                    StringBuilder sb = new StringBuilder();
                    sb.append(e.reply_count).append(" ");
                    sb.append(e.reply_count == 1 ? "reply" : "replies");
                    sb.append(": ");
                    sb.append(TextUtils.join(", ", e.reply_nicks));
                    holder.replyCount.setText(sb.toString());
                    holder.replyCount.setTextColor(ColorScheme.getInstance().isDarkTheme ? 0x66ffffff : 0x66000000);
                    holder.replyCountContainer.setVisibility(View.VISIBLE);
                    holder.replyCountContainer.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            if(getActivity() != null && getActivity() instanceof MainActivity) {
                                ((MainActivity)getActivity()).setBuffer(buffer.getBid(), e.msgid);
                            }
                        }
                    });
                } else {
                    holder.replyCountContainer.setVisibility(View.GONE);
                }
            }

            if(e.row_type == ROW_THUMBNAIL || e.row_type == ROW_FILE) {
                if(e.row_type == ROW_THUMBNAIL) {
                    if(e.entities.has("id") || e.entities.has("name") || e.entities.has("description")) {
                        holder.metadata.setVisibility(View.VISIBLE);
                        holder.thumbnailWrapper.setBackgroundColor(ColorScheme.getInstance().navBarColor);
                    } else {
                        holder.metadata.setVisibility(View.GONE);
                        holder.thumbnailWrapper.setBackgroundDrawable(null);
                    }
                    int width = getActivity().getWindowManager().getDefaultDisplay().getWidth();
                    if(getActivity().getWindowManager().getDefaultDisplay().getHeight() < width)
                        width = getActivity().getWindowManager().getDefaultDisplay().getHeight();
                    width /= 2;
                    if (e.entities.get("properties") != null && e.entities.get("properties").get("width") != null && width > e.entities.get("properties").get("width").asInt())
                        width = e.entities.get("properties").get("width").asInt();
                    try {
                        Bitmap b = null;
                        GifDrawable d = null;

                        if (e.entities.has("id") ? ImageList.getInstance().isFailedURL(e.entities.get("id").asText(), width) : ImageList.getInstance().isFailedURL(e.entities.get("thumbnail").asText())) {
                            holder.thumbnailWrapper.setVisibility(View.GONE);
                        } else{
                            if (e.entities.get("mime_type").asText().equals("image/gif")) {
                                if (e.entities.has("id"))
                                    d = ImageList.getInstance().getGIF(e.entities.get("id").asText(), width);
                                else
                                    d = ImageList.getInstance().getGIF(new URL(e.entities.get("thumbnail").asText()));
                            } else {
                                if (e.entities.has("id"))
                                    b = ImageList.getInstance().getImage(e.entities.get("id").asText(), width);
                                else
                                    b = ImageList.getInstance().getImage(new URL(e.entities.get("thumbnail").asText()), width);
                            }
                            if (b != null) {
                                float ratio = (float) b.getHeight() / (float) b.getWidth();
                                ViewGroup.LayoutParams lp = holder.thumbnail.getLayoutParams();
                                if(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, b.getWidth(), getResources().getDisplayMetrics()) >= width) {
                                    lp.width = width;
                                    lp.height = (int) (width * ratio);
                                } else {
                                    lp.width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, b.getWidth(), getResources().getDisplayMetrics());
                                    lp.height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, b.getHeight(), getResources().getDisplayMetrics());
                                }
                                holder.thumbnail.setLayoutParams(lp);
                                holder.thumbnail.setImageBitmap(b);
                                holder.thumbnail.setVisibility(View.VISIBLE);
                                holder.progress.setVisibility(View.GONE);
                            } else if (d != null) {
                                float ratio = (float) d.getCurrentFrame().getHeight() / (float) d.getCurrentFrame().getWidth();
                                ViewGroup.LayoutParams lp = holder.thumbnail.getLayoutParams();
                                if(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, d.getCurrentFrame().getWidth(), getResources().getDisplayMetrics()) >= width) {
                                    lp.width = width;
                                    lp.height = (int) (width * ratio);
                                } else {
                                    lp.width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, d.getCurrentFrame().getWidth(), getResources().getDisplayMetrics());
                                    lp.height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, d.getCurrentFrame().getHeight(), getResources().getDisplayMetrics());
                                }
                                holder.thumbnail.setLayoutParams(lp);
                                holder.thumbnail.setImageDrawable(d);
                                holder.thumbnail.setVisibility(View.VISIBLE);
                                holder.progress.setVisibility(View.GONE);
                                MultiCallback cb;
                                if (d.getCallback() != null && d.getCallback() instanceof MultiCallback)
                                    cb = (MultiCallback) d.getCallback();
                                else
                                    cb = new MultiCallback();
                                cb.addView(holder.thumbnail);
                                d.setCallback(cb);
                            } else {
                                if (e.entities.has("id")) {
                                    ImageList.getInstance().fetchImage(e.entities.get("id").asText(), width, new ImageList.OnImageFetchedListener() {
                                        @Override
                                        public void onImageFetched(Bitmap image) {
                                            refreshSoon();
                                        }
                                    });
                                } else {
                                    ImageList.getInstance().fetchImage(new URL(e.entities.get("thumbnail").asText()), width, new ImageList.OnImageFetchedListener() {
                                        @Override
                                        public void onImageFetched(Bitmap image) {
                                            refreshSoon();
                                        }
                                    }, 0);
                                }
                                holder.thumbnail.setVisibility(View.GONE);
                                holder.progress.setVisibility(View.VISIBLE);
                            }
                            holder.thumbnail.setVisibility(View.VISIBLE);
                            holder.extension.setVisibility(View.GONE);
                        }
                    } catch (FileNotFoundException e1) {
                        refreshSoon();
                    } catch (MalformedURLException e1) {
                        holder.thumbnail.setVisibility(View.GONE);
                        holder.progress.setVisibility(View.GONE);
                    } catch (IOException e1) {
                        refreshSoon();
                    } catch (OutOfMemoryError e1) {
                        String ext = "???";

                        if (e.entities.get("extension") != null) {
                            ext = e.entities.get("extension").asText();
                        } else {
                            ext = e.entities.get("mime_type").asText();
                            ext = ext.substring(ext.indexOf("/") + 1);
                        }
                        if(ext.startsWith("."))
                            ext = ext.substring(1);
                        holder.extension.setText(ext.toUpperCase());
                        holder.thumbnail.setVisibility(View.GONE);
                        holder.extension.setVisibility(View.VISIBLE);
                        holder.progress.setVisibility(View.GONE);

                        e.msg = e.msg.replace(" • ", "\n");
                    }
                    row.setContentDescription("Image thumbnail: ");
                } else {
                    String ext = "???";

                    if (e.entities.get("extension") != null) {
                        ext = e.entities.get("extension").asText();
                    } else {
                        ext = e.entities.get("mime_type").asText();
                        ext = ext.substring(ext.indexOf("/") + 1);
                    }
                    if(ext.startsWith("."))
                        ext = ext.substring(1);
                    holder.extension.setText(ext.toUpperCase());
                    holder.thumbnail.setVisibility(View.GONE);
                    holder.extension.setVisibility(View.VISIBLE);
                    holder.progress.setVisibility(View.GONE);
                    row.setContentDescription("Uploaded file: ");
                }

                if(e.entities.has("name") && e.entities.get("name") != null && e.entities.get("name").asText().length() > 0) {
                    if (mono) {
                        holder.filename.setTypeface(hackRegular);
                    } else {
                        holder.filename.setTypeface(Typeface.DEFAULT);
                    }
                    holder.filename.setText(e.entities.get("name").asText());
                    holder.filename.setVisibility(View.VISIBLE);
                    row.setContentDescription(TextUtils.concat(row.getContentDescription(), e.entities.get("name").asText(), " "));
                } else {
                    holder.filename.setVisibility(View.GONE);
                }

                if(e.msg != null && e.msg.length() > 0) {
                    holder.metadata.setMovementMethod(linkMovementMethodNoLongPress);
                    holder.metadata.setOnClickListener(new OnItemClickListener(e));
                    holder.metadata.setOnLongClickListener(new OnItemLongClickListener(e));
                    if (mono) {
                        holder.metadata.setTypeface(hackRegular);
                    } else {
                        holder.metadata.setTypeface(Typeface.DEFAULT);
                    }
                    holder.metadata.setLinkTextColor(colorScheme.linkColor);
                    holder.metadata.setTextSize(textSize);
                    holder.metadata.setText(e.formatted);
                    holder.metadata.setVisibility(View.VISIBLE);
                    if(e.formatted != null)
                        row.setContentDescription(TextUtils.concat(row.getContentDescription(), e.formatted));
                } else {
                    holder.metadata.setVisibility(View.GONE);
                }
                if(holder.thumbnailWrapper.getVisibility() == View.VISIBLE)
                    holder.timestamp.setVisibility(View.INVISIBLE);
                else
                    holder.timestamp.setVisibility(View.GONE);
                row.setBackgroundColor(e.bg_color);
            }
            return row;
        }
    }

    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        hackRegular = ResourcesCompat.getFont(getActivity(), R.font.hackregular);

        final View v = inflater.inflate(R.layout.messageview, container, false);
        statusView = v.findViewById(R.id.statusView);
        statusView.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                if (buffer != null && conn != null && server != null && server.getStatus() != null && server.getStatus().equalsIgnoreCase("disconnected")) {
                    NetworkConnection.getInstance().reconnect(buffer.getCid(), null);
                }
            }

        });

        awayView = v.findViewById(R.id.away);
        awayView.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                if(buffer != null)
                    NetworkConnection.getInstance().back(buffer.getCid(), null);
            }

        });
        awayTxt = v.findViewById(R.id.awayTxt);
        unreadBottomView = v.findViewById(R.id.unreadBottom);
        unreadBottomView.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                if(buffer != null)
                    buffer.setScrolledUp(false);
                getListView().setSelection(adapter.getCount() - 1);
                hideView(unreadBottomView);
            }

        });
        unreadBottomLabel = v.findViewById(R.id.unread);
        highlightsBottomLabel = v.findViewById(R.id.highlightsBottom);

        unreadTopView = v.findViewById(R.id.unreadTop);
        unreadTopView.setVisibility(View.GONE);
        unreadTopView.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                if (adapter.getLastSeenEIDPosition() > 0) {
                    if (heartbeatTask != null)
                        heartbeatTask.cancel(true);
                    heartbeatTask = new HeartbeatTask();
                    heartbeatTask.execute((Void) null);
                    hideView(unreadTopView);
                }
                getListView().setSelection(adapter.getLastSeenEIDPosition());
            }

        });
        unreadTopLabel = v.findViewById(R.id.unreadTopText);
        highlightsTopLabel = v.findViewById(R.id.highlightsTop);
        Button b = v.findViewById(R.id.markAsRead);
        b.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                hideView(unreadTopView);
                if (heartbeatTask != null)
                    heartbeatTask.cancel(true);
                heartbeatTask = new HeartbeatTask();
                heartbeatTask.execute((Void) null);
            }
        });
        globalMsgView = v.findViewById(R.id.globalMessageView);
        globalMsg = v.findViewById(R.id.globalMessageTxt);
        b = v.findViewById(R.id.dismissGlobalMessage);
        b.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (conn != null)
                    conn.globalMsg = null;
                update_global_msg();
            }
        });
        globalMsg.setMovementMethod(IRCCloudLinkMovementMethod.getInstance());
        spinner = v.findViewById(R.id.spinner);
        suggestionsContainer = v.findViewById(R.id.suggestionsContainer);
        suggestions = v.findViewById(R.id.suggestions);
        headerViewContainer = getLayoutInflater(null).inflate(R.layout.messageview_header, null);
        headerView = headerViewContainer.findViewById(R.id.progress);
        backlogFailed = headerViewContainer.findViewById(R.id.backlogFailed);
        loadBacklogButton = headerViewContainer.findViewById(R.id.loadBacklogButton);
        loadBacklogButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (conn != null && buffer != null) {
                    backlogFailed.setVisibility(View.GONE);
                    loadBacklogButton.setVisibility(View.GONE);
                    headerView.setVisibility(View.VISIBLE);
                    requestingBacklog = true;
                    NetworkConnection.getInstance().request_backlog(buffer.getCid(), buffer.getBid(), earliest_eid);
                }
            }
        });
        ((ListView) v.findViewById(android.R.id.list)).addHeaderView(headerViewContainer);
        footerViewContainer = new View(getActivity());
        ((ListView) v.findViewById(android.R.id.list)).addFooterView(footerViewContainer, null, false);
        avatarContainer = v.findViewById(R.id.avatarContainer);
        avatar = v.findViewById(R.id.avatar);
        return v;
    }

    public void showSpinner(boolean show) {
        if (show) {
            spinner.setAlpha(0);
            spinner.animate().alpha(1);
            spinner.setVisibility(View.VISIBLE);
        } else {
            spinner.animate().alpha(0).withEndAction(new Runnable() {
                @Override
                public void run() {
                    spinner.setVisibility(View.GONE);
                }
            });
        }
    }

    private void hideView(final View v) {
        if (v.getVisibility() != View.GONE) {
            v.animate().alpha(0).setDuration(100).withEndAction(new Runnable() {
                @Override
                public void run() {
                    v.setVisibility(View.GONE);
                }
            });
        }
    }

    private void showView(final View v) {
        if (v.getVisibility() != View.VISIBLE) {
            v.setAlpha(0);
            v.animate().alpha(1).setDuration(100);
            v.setVisibility(View.VISIBLE);
        }
    }

    public void drawerClosed() {
        try {
            ListView v = getListView();
            mOnScrollListener.onScroll(v, v.getFirstVisiblePosition(), v.getLastVisiblePosition() - v.getFirstVisiblePosition(), adapter.getCount());
        } catch (Exception e) {
        }
    }

    private void hide_avatar() {
        if (avatarContainer.getVisibility() != View.GONE)
            avatarContainer.setVisibility(View.GONE);
        if (avatar.getTag() != null) {
            ((ImageView) avatar.getTag()).setVisibility(View.VISIBLE);
            avatar.setTag(null);
        }
    }

    private OnScrollListener mOnScrollListener = new OnScrollListener() {
        @Override
        public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount, final int totalItemCount) {
            if (!ready || buffer == null || adapter == null || visibleItemCount < 0) {
                hide_avatar();
                return;
            }

            if (firstVisibleItem > 0)
                fetch_if_needed = false;

            if (conn.ready && !requestingBacklog && headerView != null && buffer.getMin_eid() > 0) {
                if (firstVisibleItem == 0 && visibleItemCount > 1 && headerView.getVisibility() == View.VISIBLE && conn.getState() == NetworkConnection.STATE_CONNECTED) {
                    requestingBacklog = true;
                    conn.request_backlog(buffer.getCid(), buffer.getBid(), earliest_eid);
                    hide_avatar();
                    return;
                } else if(firstVisibleItem > 0 && loadBacklogButton.getVisibility() == View.VISIBLE) {
                    loadBacklogButton.setVisibility(View.GONE);
                    headerView.setVisibility(View.VISIBLE);
                }
            }

            if (unreadBottomView != null && adapter.data.size() > 0) {
                if (firstVisibleItem + visibleItemCount >= totalItemCount) {
                    if(unreadBottomView.getVisibility() != View.GONE)
                        unreadBottomView.setVisibility(View.GONE);
                    if (unreadTopView.getVisibility() == View.GONE && conn.getState() == NetworkConnection.STATE_CONNECTED) {
                        if (heartbeatTask != null)
                            heartbeatTask.cancel(true);
                        heartbeatTask = new HeartbeatTask();
                        heartbeatTask.execute((Void) null);
                    }
                    newMsgs = 0;
                    newMsgTime = 0;
                    newHighlights = 0;
                }
            }
            if (firstVisibleItem + visibleItemCount < totalItemCount - 1) {
                View v = view.getChildAt(0);
                buffer.setScrolledUp(true);
                buffer.setScrollPosition(firstVisibleItem);
                buffer.setScrollPositionOffset((v == null) ? 0 : v.getTop());
            } else {
                buffer.setScrolledUp(false);
                buffer.setScrollPosition(-1);
            }

            if (adapter != null && adapter.data.size() > 0) {
                synchronized (adapter.data) {
                    if (unreadTopView != null && unreadTopView.getVisibility() == View.VISIBLE) {
                        update_top_unread(firstVisibleItem);
                        int markerPos = -1;
                        if (adapter != null)
                            markerPos = adapter.getLastSeenEIDPosition();
                        if (markerPos > 1 && getListView().getFirstVisiblePosition() <= markerPos) {
                            unreadTopView.setVisibility(View.GONE);
                            if (conn.getState() == NetworkConnection.STATE_CONNECTED) {
                                if (heartbeatTask != null)
                                    heartbeatTask.cancel(true);
                                heartbeatTask = new HeartbeatTask();
                                heartbeatTask.execute((Void) null);
                            }
                        }
                    }

                    if (!pref_chatOneLine && !pref_avatarsOff && firstVisibleItem > ((ListView) view).getHeaderViewsCount()) {
                        int offset = (unreadTopView.getVisibility() == View.VISIBLE) ? unreadTopView.getHeight() : 0;
                        View v;
                        int i = 0;
                        do {
                            v = view.getChildAt(i++);
                        } while (v != null && v.getTop() + v.getHeight() <= offset - 1);
                        if(v != null) {
                            int first = firstVisibleItem - ((ListView) view).getHeaderViewsCount() + i - 1;
                            MessageAdapter.ViewHolder vh = (MessageAdapter.ViewHolder) v.getTag();
                            MessageAdapter.ViewHolder top_vh = vh;
                            Event e = first >= 0 ? (Event) adapter.getItem(first) : null;
                            if (first > 0 && vh != null && v.getTop() <= offset && e != null && ((vh.avatar != null && e.group_eid < 1 && e.isMessage() && e.from != null && e.from.length() > 0) || e.row_type == ROW_LASTSEENEID)) {
                                for (i = first; i < adapter.getCount() && i < first + 4; i++) {
                                    e = (Event) adapter.getItem(i);
                                    if(e.row_type == ROW_LASTSEENEID)
                                        e = (Event) adapter.getItem(i-1);
                                    int next = i + 1;
                                    Event e1 = (Event) adapter.getItem(next);
                                    if(e1 != null && e1.row_type == ROW_LASTSEENEID) {
                                        next++;
                                        if(next < adapter.getCount())
                                            e1 = (Event) adapter.getItem(next);
                                        else
                                            break;
                                    }
                                    if (e != null && e1 != null && e.from != null && e.from.equals(e1.from) && e1.group_eid < 1 && !e1.header) {
                                        View v1 = view.getChildAt(next - (firstVisibleItem - ((ListView) view).getHeaderViewsCount()));
                                        if (v1 != null) {
                                            MessageAdapter.ViewHolder vh1 = (MessageAdapter.ViewHolder) v1.getTag();
                                            if (vh1 != null && vh1.avatar != null) {
                                                v = v1;
                                                vh = vh1;
                                            }
                                        }
                                    } else {
                                        break;
                                    }
                                }
                                if (avatar.getTag() != null && avatar.getTag() != top_vh.avatar) {
                                    ((ImageView) avatar.getTag()).setVisibility(View.VISIBLE);
                                }

                                if(e != null) {
                                    final int width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 32, getResources().getDisplayMetrics());
                                    Avatar a = null;
                                    Bitmap b = null;
                                    URL avatarURL = null;
                                    try {
                                        if(pref_avatarImages && e.getAvatarURL(width) != null)
                                            avatarURL = new URL(e.getAvatarURL(width));
                                    } catch (Exception e1) {
                                        e1.printStackTrace();
                                    }
                                    if(avatarURL != null) {
                                        try {
                                            b = ImageList.getInstance().getImage(avatarURL);
                                            if(b == null)
                                                ImageList.getInstance().fetchImage(avatarURL, new ImageList.OnImageFetchedListener() {
                                                    @Override
                                                    public void onImageFetched(Bitmap image) {
                                                        if(image != null) {
                                                            runOnUiThread(new Runnable() {
                                                                @Override
                                                                public void run() {
                                                                    try {
                                                                        ListView v = getListView();
                                                                        mOnScrollListener.onScroll(v, v.getFirstVisiblePosition(), v.getLastVisiblePosition() - v.getFirstVisiblePosition(), adapter.getCount());
                                                                    } catch (Exception e) {
                                                                    }
                                                                }
                                                            });
                                                        }
                                                    }
                                                });
                                        } catch (IOException e1) {
                                        }
                                    }
                                    if(b == null) {
                                        a = mAvatarsList.getAvatar(e.cid, e.from_nick, e.from);
                                        b = a.getBitmap(ColorScheme.getInstance().isDarkTheme, width, e.self);
                                    }
                                    if(vh.avatar != null && b != null) {
                                        int topMargin, leftMargin;
                                        int height = b.getHeight() + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics());
                                        if (v.getHeight() + v.getTop() < (height + offset)) {
                                            topMargin = v.getTop() + v.getHeight() - height;
                                        } else {
                                            topMargin = offset;
                                        }
                                        leftMargin = vh.avatar.getLeft();
                                        avatarContainer.offset(leftMargin, topMargin);
                                        avatar.setImageBitmap(b);
                                        if (top_vh.avatar == null || (avatar.getTag() != top_vh.avatar || top_vh.avatar.getVisibility() != View.INVISIBLE)) {
                                            avatar.setTag(top_vh.avatar);
                                            avatarContainer.setVisibility(View.VISIBLE);
                                            if(top_vh.avatar != null)
                                                top_vh.avatar.setVisibility(View.INVISIBLE);
                                        }
                                    } else {
                                        hide_avatar();
                                    }
                                }
                            } else {
                                hide_avatar();
                            }
                        } else {
                            hide_avatar();
                        }
                    } else {
                        hide_avatar();
                    }
                }
            } else {
                hide_avatar();
            }
        }

        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
        }

    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if(tapTimer == null)
            tapTimer = new Timer("message-tap-timer");
        conn = NetworkConnection.getInstance();
        conn.addHandler(this);
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mListener = (MessageViewListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString() + " must implement MessageViewListener");
        }
        if(tapTimer == null)
            tapTimer = new Timer("message-tap-timer");
    }

    @Override
    public synchronized void setArguments(Bundle args) {
        ready = false;
        if(args == null)
            return;
        if (heartbeatTask != null)
            heartbeatTask.cancel(true);
        heartbeatTask = null;
        if (tapTimerTask != null)
            tapTimerTask.cancel();
        tapTimerTask = null;
        if(tapTimer == null)
            tapTimer = new Timer("message-tap-timer");
        if (buffer != null && args != null && buffer.getBid() != args.getInt("bid", -1) && adapter != null)
            adapter.clearLastSeenEIDMarker();
        if(args != null)
            buffer = BuffersList.getInstance().getBuffer(args.getInt("bid", -1));
        if (buffer != null) {
            server = buffer.getServer();
            IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "MessageViewFragment: switched to bid: " + buffer.getBid());
        } else {
            IRCCloudLog.Log(Log.WARN, "IRCCloud", "MessageViewFragment: couldn't find buffer to switch to");
        }
        if(args != null)
            msgid = args.getString("msgid");
        requestingBacklog = false;
        avgInsertTime = 0;
        newMsgs = 0;
        newMsgTime = 0;
        newHighlights = 0;
        earliest_eid = 0;
        backlog_eid = 0;
        currentCollapsedEid = -1;
        lastCollapsedDay = -1;
        if (server != null) {
            ignore = server.ignores;
            if (server.getAway() != null && server.getAway().length() > 0) {
                awayTxt.setText(ColorFormatter.strip("Away (" + server.getAway() + ")"));
                awayView.setVisibility(View.VISIBLE);
            } else {
                awayView.setVisibility(View.GONE);
            }
            collapsedEvents.setServer(server);
            update_status(server.getStatus(), server.getFail_info());
        }
        if (avatar != null) {
            avatarContainer.setVisibility(View.GONE);
            avatar.setTag(null);
        }
        if (unreadTopView != null)
            unreadTopView.setVisibility(View.GONE);
        backlogFailed.setVisibility(View.GONE);
        loadBacklogButton.setVisibility(View.GONE);
        try {
            if (getListView().getHeaderViewsCount() == 0) {
                getListView().addHeaderView(headerViewContainer);
            }
        } catch (IllegalStateException e) {
        }
        ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) headerView.getLayoutParams();
        lp.topMargin = 0;
        headerView.setLayoutParams(lp);
        lp = (ViewGroup.MarginLayoutParams) backlogFailed.getLayoutParams();
        lp.topMargin = 0;
        backlogFailed.setLayoutParams(lp);
        if (buffer != null && EventsList.getInstance().getEventsForBuffer(buffer.getBid()) != null) {
            if (refreshTask != null)
                refreshTask.cancel(true);
            refreshTask = new RefreshTask();
            if (args != null && args.getBoolean("fade")) {
                IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "MessageViewFragment: Loading message contents in the background");
                refreshTask.execute((Void) null);
            } else {
                IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "MessageViewFragment: Loading message contents");
                refreshTask.onPreExecute();
                refreshTask.onPostExecute(refreshTask.doInBackground());
            }
        } else {
            if (buffer == null || buffer.getMin_eid() == 0 || earliest_eid == buffer.getMin_eid() || conn.getState() != NetworkConnection.STATE_CONNECTED || !conn.ready || (msgid != null && msgids.containsKey(msgid))) {
                headerView.setVisibility(View.GONE);
                loadBacklogButton.setVisibility(View.GONE);
            } else if (buffer != null && buffer.getDeferred() > 0) {
                backlogFailed.setVisibility(View.GONE);
                loadBacklogButton.setVisibility(View.GONE);
                headerView.setVisibility(View.VISIBLE);
                requestingBacklog = true;
                buffer.setDeferred(0);
                NetworkConnection.getInstance().request_backlog(buffer.getCid(), buffer.getBid(), earliest_eid);
            } else {
                headerView.setVisibility(View.GONE);
                loadBacklogButton.setVisibility(View.VISIBLE);
            }
            if (adapter != null) {
                adapter.clear();
                adapter.notifyDataSetInvalidated();
            } else {
                adapter = new MessageAdapter(MessageViewFragment.this, 0);
                setListAdapter(adapter);
            }
            if(mListener != null)
                mListener.onMessageViewReady();
            ready = true;
        }
    }

    private void runOnUiThread(Runnable r) {
        if (getActivity() != null)
            getActivity().runOnUiThread(r);
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        IRCCloudLog.Log(Log.DEBUG, "IRCCloud", "Received low memory warning in the foreground, cleaning backlog in other buffers");
        for (Buffer b : BuffersList.getInstance().getBuffers()) {
            if (b != buffer)
                EventsList.getInstance().pruneEvents(b.getBid());
        }
    }

    private void insertEvent(final MessageAdapter messageAdapter, final Event event, final boolean backlog, boolean nextIsGrouped) {
        synchronized (MessageViewFragment.class) {
            event.ready_for_display = false;
            try {
                long start = System.currentTimeMillis();
                if (event.eid <= buffer.getMin_eid() || (msgid != null && msgids.containsKey(msgid))) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            headerView.setVisibility(View.GONE);
                            backlogFailed.setVisibility(View.GONE);
                            loadBacklogButton.setVisibility(View.GONE);
                        }
                    });
                }
                if (earliest_eid == 0 || event.eid < earliest_eid)
                    earliest_eid = event.eid;

                if(msgid != null && msgid.length() > 0 && !(msgid.equals(event.msgid) || msgid.equals(event.reply())))
                    return;

                if(buffer.getType() != null && buffer.getType().equals("conversation") && !buffer.isMPDM() && !event.self) {
                    buffer_usermask = event.from_nick + "!" + event.hostmask;
                }

                String type = event.type;
                long eid = event.eid;

                if (type.startsWith("you_"))
                    type = type.substring(4);

                if (type.equals("joined_channel") || type.equals("parted_channel") || type.equals("nickchange") || type.equals("quit") || type.equals("user_channel_mode") || type.equals("socket_closed") || type.equals("connecting_cancelled") || type.equals("connecting_failed")) {
                    collapsedEvents.showChan = !buffer.isChannel();
                    if (pref_hideJoinPart && !type.equals("socket_closed") && !type.equals("connecting_cancelled") && !type.equals("connecting_failed") && !event.self) {
                        messageAdapter.removeItem(event.eid);
                        if (!backlog)
                            messageAdapter.notifyDataSetChanged();
                        return;
                    }

                    Calendar calendar = Calendar.getInstance();
                    calendar.setTimeInMillis(event.getTime());

                    if (pref_expandJoinPart)
                        expandedSectionEids.clear();

                    if (event.type.equals("socket_closed") || event.type.equals("connecting_failed") || event.type.equals("connecting_cancelled")) {
                        Event last = EventsList.getInstance().getEvent(lastCollapsedEid, buffer.getBid());
                        if (last != null && !last.type.equals("socket_closed") && !last.type.equals("connecting_failed") && !last.type.equals("connecting_cancelled"))
                            currentCollapsedEid = -1;
                    } else {
                        Event last = EventsList.getInstance().getEvent(lastCollapsedEid, buffer.getBid());
                        if (last != null && (last.type.equals("socket_closed") || last.type.equals("connecting_failed") || last.type.equals("connecting_cancelled")))
                            currentCollapsedEid = -1;
                    }

                    if (currentCollapsedEid == -1 || calendar.get(Calendar.DAY_OF_YEAR) != lastCollapsedDay || pref_expandJoinPart || event.type.equals("you_parted_channel")) {
                        collapsedEvents.clear();
                        currentCollapsedEid = eid;
                        lastCollapsedDay = calendar.get(Calendar.DAY_OF_YEAR);
                    }

                    if (!collapsedEvents.showChan)
                        event.chan = buffer.getName();

                    if (!collapsedEvents.addEvent(event))
                        collapsedEvents.clear();

                    if ((currentCollapsedEid == event.eid || pref_expandJoinPart) && type.equals("user_channel_mode")) {
                        event.color = colorScheme.messageTextColor;
                        event.bg_color = colorScheme.collapsedHeadingBackgroundColor;
                    } else {
                        event.color = colorScheme.collapsedRowTextColor;
                        event.bg_color = colorScheme.contentBackgroundColor;
                    }

                    String msg;
                    if (expandedSectionEids.contains(currentCollapsedEid)) {
                        CollapsedEventsList c = new CollapsedEventsList();
                        c.showChan = collapsedEvents.showChan;
                        c.setServer(server);
                        c.addEvent(event);
                        msg = c.getCollapsedMessage();
                        if (!nextIsGrouped) {
                            event.contentDescription = ColorFormatter.html_to_spanned(ColorFormatter.irc_to_html(c.getCollapsedMessage(true)), true, server);
                            String group_msg = collapsedEvents.getCollapsedMessage();
                            if (group_msg == null && type.equals("nickchange")) {
                                group_msg = event.old_nick + " → <b>" + event.nick + "</b>";
                            }
                            if (group_msg == null && type.equals("user_channel_mode")) {
                                if (event.from != null && event.from.length() > 0)
                                    msg = collapsedEvents.formatNick(event.nick, null, event.target_mode, false) + " was set to <b>" + event.diff + "</b> by <b>" + collapsedEvents.formatNick(event.from, null, event.from_mode, false) + "</b>";
                                else
                                    msg = collapsedEvents.formatNick(event.nick, null, event.target_mode, false) + " was set to <b>" + event.diff + "</b> by the server <b>" + event.server + "</b>";
                                currentCollapsedEid = eid;
                            }
                            Event heading = new Event();
                            heading.type = "__expanded_group_heading__";
                            heading.cid = event.cid;
                            heading.bid = event.bid;
                            heading.eid = currentCollapsedEid - 1;
                            heading.group_msg = group_msg;
                            heading.contentDescription = ColorFormatter.html_to_spanned(ColorFormatter.irc_to_html(collapsedEvents.getCollapsedMessage(true)), true, server);
                            heading.color = colorScheme.timestampColor;
                            heading.bg_color = colorScheme.contentBackgroundColor;
                            heading.linkify = false;
                            heading.html = ColorFormatter.irc_to_html(group_msg);
                            heading.formatted = ColorFormatter.html_to_spanned(heading.html);
                            messageAdapter.addItem(currentCollapsedEid - 1, heading);
                            if (event.type.equals("socket_closed") || event.type.equals("connecting_failed") || event.type.equals("connecting_cancelled")) {
                                Event last = EventsList.getInstance().getEvent(lastCollapsedEid, buffer.getBid());
                                if (last != null)
                                    last.row_type = ROW_MESSAGE;
                                event.row_type = ROW_SOCKETCLOSED;
                            }
                        }
                        event.timestamp = null;
                    } else {
                        msg = (nextIsGrouped && currentCollapsedEid != event.eid) ? "" : collapsedEvents.getCollapsedMessage();
                    }

                    if (!nextIsGrouped && !expandedSectionEids.contains(currentCollapsedEid))
                        event.contentDescription = ColorFormatter.html_to_spanned(ColorFormatter.irc_to_html(collapsedEvents.getCollapsedMessage(true)), false, server);
                    if (msg == null && type.equals("nickchange")) {
                        msg = event.old_nick + " → <b>" + event.nick + "</b>";
                        event.contentDescription = ColorFormatter.html_to_spanned(ColorFormatter.irc_to_html(event.old_nick + " notify nickname to " + event.nick));
                    }
                    if (msg == null && type.equals("user_channel_mode")) {
                        if (event.from != null && event.from.length() > 0)
                            msg = collapsedEvents.formatNick(event.nick, null, event.target_mode, false) + " was set to <b>" + event.diff + "</b> by <b>" + collapsedEvents.formatNick(event.from, null, event.from_mode, false) + "</b>";
                        else
                            msg = collapsedEvents.formatNick(event.nick, null, event.target_mode, false) + " was set to <b>" + event.diff + "</b> by the server <b>" + event.server + "</b>";
                        currentCollapsedEid = eid;
                        event.contentDescription = ColorFormatter.html_to_spanned(msg);
                    }
                    if (!expandedSectionEids.contains(currentCollapsedEid)) {
                        if (eid != currentCollapsedEid) {
                            event.color = colorScheme.timestampColor;
                            event.bg_color = colorScheme.contentBackgroundColor;
                        }
                        eid = currentCollapsedEid;
                    }
                    event.group_msg = msg;
                    event.html = null;
                    event.formatted = null;
                    event.linkify = false;
                    lastCollapsedEid = event.eid;
                    if ((buffer.isConsole() && !event.type.equals("socket_closed") && !event.type.equals("connecting_failed") && !event.type.equals("connecting_cancelled")) || event.type.equals("you_parted_channel")) {
                        currentCollapsedEid = -1;
                        lastCollapsedEid = -1;
                        collapsedEvents.clear();
                    }
                } else {
                    currentCollapsedEid = -1;
                    lastCollapsedEid = -1;
                    collapsedEvents.clear();

                    synchronized(formatLock) {
                        if (event.html == null) {
                            String msg = event.msg;
                            if (!pref_disableLargeEmoji && ColorFormatter.is_emoji(ColorFormatter.emojify(msg)))
                                msg = "<large>" + msg + "</large>";

                            if ((pref_chatOneLine || !event.isMessage()) && event.from != null && event.from.length() > 0)
                                event.html_prefix = "<b>" + collapsedEvents.formatNick(event.from_nick, event.from, event.from_mode, !event.self && pref_nickColors, ColorScheme.getInstance().selfTextColor) + "</b> ";
                            else if (pref_chatOneLine && event.type.equals("buffer_msg") && event.server != null && event.server.length() > 0)
                                event.html_prefix = "<b>" + event.server + "</b> ";
                            event.html = msg;
                            if (event.html != null && event.msg != null)
                                event.mention_offset = event.html.length() - event.msg.length();
                            if (event.type.equals("channel_topic") && event.msg.startsWith("set the topic: "))
                                event.mention_offset += 15;
                        }
                    }
                }

                String collapsedNickColor = Integer.toHexString(ColorScheme.getInstance().collapsedRowNickColor).substring(2);
                String msg;
                String from = event.from_nick;
                if (from == null || from.length() == 0)
                    from = event.nick;

                if (from != null && event.hostmask != null && event.isMessage() && buffer.getType() != null && !buffer.isConversation()) {
                    String usermask = from + "!" + event.hostmask;
                    if (ignore.match(usermask)) {
                        if (unreadTopView != null && unreadTopView.getVisibility() == View.GONE && unreadBottomView != null && unreadBottomView.getVisibility() == View.GONE) {
                            if (heartbeatTask != null)
                                heartbeatTask.cancel(true);
                            heartbeatTask = new HeartbeatTask();
                            heartbeatTask.execute((Void) null);
                        }
                        return;
                    }
                }

                if(event.row_type == ROW_THUMBNAIL) {
                    event.html = event.msg;
                } else {
                    synchronized (formatLock) {
                        switch (type) {
                            case "channel_mode":
                                if (event.nick != null && event.nick.length() > 0)
                                    event.html = event.msg + " by <b>" + collapsedEvents.formatNick(event.nick, null, event.from_mode, false) + "</b>";
                                else if (event.server != null && event.server.length() > 0)
                                    event.html = event.msg + " by the server <b>" + event.server + "</b>";
                                break;
                            case "buffer_me_msg":
                                msg = event.msg;
                                if (!pref_disableLargeEmoji && ColorFormatter.is_emoji(ColorFormatter.emojify(msg)))
                                    msg = "<large>" + msg + "</large>";
                                event.html_prefix = "— <i><b>" + collapsedEvents.formatNick(event.from_nick, event.nick, event.from_mode, !event.self && pref_nickColors, ColorScheme.getInstance().selfTextColor) + "</b> ";
                                event.html = "<i>" + msg;
                                event.mention_offset += 3;
                                break;
                            case "buffer_msg":
                            case "notice":
                                event.code_block = false;
                                event.mention_offset = type.equals("notice") ? 5 : 0;
                                event.color = ColorScheme.getInstance().messageTextColor;
                                msg = event.msg;
                                if (!pref_disableLargeEmoji && ColorFormatter.is_emoji(ColorFormatter.emojify(msg)))
                                    msg = "<large>" + msg + "</large>";

                                if (!pref_disableCodeBlock) {
                                    String original_msg = msg;
                                    Matcher m = IS_CODE_BLOCK.matcher(original_msg);
                                    int pos = -1;
                                    String lastChunk = "";

                                    while (m.find()) {
                                        if (pos == -1)
                                            pos = 0;

                                        if (m.start() > 0)
                                            lastChunk = original_msg.substring(pos, m.start());
                                        boolean strippedSpace = false;
                                        if (lastChunk.startsWith(" ") && lastChunk.length() > 1) {
                                            lastChunk = lastChunk.substring(1);
                                            strippedSpace = true;
                                        }

                                        if (pos > 0) {
                                            Event e = new Event(event);
                                            e.html = lastChunk;
                                            e.timestamp = "";
                                            e.header = false;
                                            e.parent_eid = eid;
                                            if (!pref_disableCodeSpan) {
                                                e.html = ColorFormatter.insert_codespans(e.html);
                                            }
                                            e.mention_offset = -pos;
                                            if (strippedSpace)
                                                e.mention_offset--;
                                            e.ready_for_display = true;
                                            messageAdapter.insertBelow(eid, e, backlog);
                                        } else {
                                            msg = lastChunk;
                                            messageAdapter.addItem(event.eid, event);
                                        }

                                        if (m.start() == 0 && !pref_chatOneLine) {
                                            msg = original_msg.substring(3, m.end() - 3);
                                            if(msg.startsWith("\n"))
                                                msg = msg.substring(1);
                                            if(msg.endsWith("\n"))
                                                msg = msg.substring(0, msg.length() - 1);
                                            event.code_block = true;
                                            event.color = ColorScheme.getInstance().codeSpanForegroundColor;
                                            event.mention_offset = -3;
                                            messageAdapter.addItem(event.eid, event);
                                        } else {
                                            Event e = new Event(event);
                                            e.html = original_msg.substring(m.start() + 3, m.end() - 3);
                                            if(e.html.startsWith("\n"))
                                                e.html = e.html.substring(1);
                                            if(e.html.endsWith("\n"))
                                                e.html = e.html.substring(0, e.html.length() - 1);
                                            e.timestamp = "";
                                            e.code_block = true;
                                            e.color = ColorScheme.getInstance().codeSpanForegroundColor;
                                            e.header = false;
                                            e.parent_eid = eid;
                                            e.mention_offset = -(m.start() + 3);
                                            e.ready_for_display = true;
                                            messageAdapter.insertBelow(eid, e, backlog);
                                        }

                                        pos = m.end();
                                    }

                                    if (pos > 0 && pos < original_msg.length()) {
                                        Event e = new Event(event);
                                        e.html = original_msg.substring(pos);
                                        boolean strippedSpace = false;
                                        if (e.html.startsWith(" ")) {
                                            e.html = e.html.substring(1);
                                            strippedSpace = true;
                                        }
                                        if (!pref_disableCodeSpan) {
                                            e.html = ColorFormatter.insert_codespans(e.html);
                                        }
                                        e.timestamp = "";
                                        e.header = false;
                                        e.parent_eid = eid;
                                        e.mention_offset = -pos;
                                        if (strippedSpace)
                                            e.mention_offset--;
                                        e.ready_for_display = true;
                                        if (e.html.length() > 0)
                                            messageAdapter.insertBelow(eid, e, backlog);
                                    }
                                }

                                if (!pref_disableCodeSpan) {
                                    msg = ColorFormatter.insert_codespans(msg);
                                }

                                String html = "";
                                if (event.target_mode != null && server != null && server.PREFIX != null) {
                                    if (server.PREFIX.has(server.MODE_OPER) && server.PREFIX.get(server.MODE_OPER) != null && event.target_mode.equals(server.PREFIX.get(server.MODE_OPER).asText()))
                                        html += collapsedEvents.formatNick("Opers", null, server.MODE_OPER, false) + " ";
                                    else if (server.PREFIX.has(server.MODE_OWNER) && server.PREFIX.get(server.MODE_OWNER) != null && event.target_mode.equals(server.PREFIX.get(server.MODE_OWNER).asText()))
                                        html += collapsedEvents.formatNick("Owners", null, server.MODE_OWNER, false) + " ";
                                    else if (server.PREFIX.has(server.MODE_ADMIN) && server.PREFIX.get(server.MODE_ADMIN) != null && event.target_mode.equals(server.PREFIX.get(server.MODE_ADMIN).asText()))
                                        html += collapsedEvents.formatNick("Admins", null, server.MODE_ADMIN, false) + " ";
                                    else if (server.PREFIX.has(server.MODE_OP) && server.PREFIX.get(server.MODE_OP) != null && event.target_mode.equals(server.PREFIX.get(server.MODE_OP).asText()))
                                        html += collapsedEvents.formatNick("Ops", null, server.MODE_OP, false) + " ";
                                    else if (server.PREFIX.has(server.MODE_HALFOP) && server.PREFIX.get(server.MODE_HALFOP) != null && event.target_mode.equals(server.PREFIX.get(server.MODE_HALFOP).asText()))
                                        html += collapsedEvents.formatNick("Half Ops", null, server.MODE_HALFOP, false) + " ";
                                    else if (server.PREFIX.has(server.MODE_VOICED) && server.PREFIX.get(server.MODE_VOICED) != null && event.target_mode.equals(server.PREFIX.get(server.MODE_VOICED).asText()))
                                        html += collapsedEvents.formatNick("Voiced", null, server.MODE_VOICED, false) + " ";
                                }
                                if (buffer.isConsole() && event.to_chan && event.chan != null && event.chan.length() > 0) {
                                    html += "<b>" + event.chan + "</b>: ";
                                } else if (buffer.isConsole() && event.self && event.nick != null && event.nick.length() > 0) {
                                    html += "<b>" + event.nick + "</b>: ";
                                }

                                if (pref_chatOneLine && event.from != null && event.from.length() > 0) {
                                    if (!pref_disableQuote && event.msg != null && event.msg.length() > 0 && ColorFormatter.is_blockquote(ColorFormatter.html_to_spanned(event.msg).toString())) {
                                        Event e = new Event(event);
                                        e.timestamp = "";
                                        e.html = msg;
                                        e.parent_eid = event.eid;
                                        html = null;
                                        event.html = msg = "<b>" + collapsedEvents.formatNick(event.from_nick, event.from, event.from_mode, !event.self && pref_nickColors, ColorScheme.getInstance().selfTextColor) + "</b>";
                                        messageAdapter.addItem(event.eid, event);
                                        e.day = event.day;
                                        messageAdapter.insertBelow(event.eid, e, backlog);
                                    } else {
                                        html = "<b>" + collapsedEvents.formatNick(event.from_nick, event.from, event.from_mode, !event.self && pref_nickColors, ColorScheme.getInstance().selfTextColor) + "</b> " + html;
                                    }
                                }
                                event.html_prefix = html;
                                event.html = msg;
                                break;
                            case "kicked_channel":
                                event.html_prefix = "\u0004" + collapsedNickColor + "← \u000f";
                                if (event.type.startsWith("you_"))
                                    event.html_prefix += "You";
                                else
                                    event.html_prefix += "<b>" + collapsedEvents.formatNick(event.old_nick, null, null, false, collapsedNickColor) + "</b>";
                                if (event.hostmask != null && event.hostmask.length() > 0)
                                    event.html_prefix += " (" + event.hostmask + ")";
                                if (event.type.startsWith("you_"))
                                    event.html_prefix += " were";
                                else
                                    event.html_prefix += " was";
                                if (event.from_hostmask != null && event.from_hostmask.length() > 0)
                                    event.html_prefix += " kicked by " + collapsedEvents.formatNick(event.nick, null, event.from_mode, false, collapsedNickColor);
                                else
                                    event.html_prefix += " kicked by the server \u0004" + collapsedNickColor + event.nick + "\u000f";
                                if (event.msg != null && event.msg.length() > 0 && !event.msg.equals(event.nick))
                                    event.html = ": " + (pref_noColor ? ColorFormatter.strip_colors(event.msg) : event.msg);
                                else
                                    event.html = "";
                                break;
                            case "callerid":
                                event.html_prefix = "<b>" + collapsedEvents.formatNick(event.from_nick, event.from, event.from_mode, false) + "</b> (" + event.hostmask + ") ";
                                event.html = event.msg + " Tap to accept.";
                                break;
                            case "channel_mode_list_change":
                                if (event.from.length() == 0) {
                                    if (event.nick != null && event.nick.length() > 0)
                                        event.html = "<b>" + collapsedEvents.formatNick(event.nick, null, event.from_mode, false) + "</b> " + event.msg;
                                    else if (event.server != null && event.server.length() > 0)
                                        event.html = "The server <b>" + event.server + "</b> " + event.msg;
                                }
                                break;
                            case "user_chghost":
                                event.html = "<b>" + collapsedEvents.formatNick(event.nick, null, event.from_mode, false, collapsedNickColor) + "</b> " + event.msg;
                                break;
                            case "channel_name_change":
                                if (event.from.length() == 0) {
                                    if (event.server != null && event.server.length() > 0)
                                        event.html = "The server <b>" + event.server + "</b> " + event.msg;
                                    else
                                        event.html = "The server " + event.msg;
                                } else {
                                    event.html = "<b>" + collapsedEvents.formatNick(event.from_nick, event.from, event.from_mode, false, Integer.toHexString(ColorScheme.getInstance().collapsedRowNickColor).substring(2)) + "</b> " + event.msg;
                                }
                                break;
                        }
                    }
                }

                if(event.msgid != null && event.msgid.length() > 0)
                    msgids.put(event.msgid, event);

                if(event.reply() != null && event.reply().length() > 0) {
                    Event parent = msgids.get(event.reply());
                    if(parent != null) {
                        parent.reply_count++;
                        if (parent.reply_nicks == null)
                            parent.reply_nicks = new HashSet<>();
                        parent.reply_nicks.add(event.from);
                    }
                }
                event.is_reply = (event.reply() != null && event.reply().length() > 0);
                if(event.is_reply && pref_replyCollapse) {
                    if(!backlog)
                        messageAdapter.notifyDataSetChanged();
                    return;
                }

                event.ready_for_display = true;
                if(!backlog)
                    messageAdapter.format(event);
                messageAdapter.addItem(eid, event);

                if(!event.pending && pref_inlineImages && event.type.equals("buffer_msg") && event.msg.length() > 0 && event.row_type != ROW_THUMBNAIL) {
                    Matcher m = ColorFormatter.WEB_URL.matcher(event.msg);

                    while(m.find()) {
                        String url = event.msg.substring(m.start(), m.end());
                        if(!url.startsWith("http://") && !url.startsWith("https://"))
                            url = "http://" + url;
                        if(!hiddenFileIDs.contains(url)) {
                            boolean found = false;
                            if (event.entities != null && event.entities.has("files")) {
                                JsonNode files = event.entities.get("files");

                                for (int i = 0; i < files.size(); i++) {
                                    JsonNode entity = files.get(i);
                                    String fileID = entity.get("id").asText();
                                    if (url.contains("/file/" + fileID + "/")) {
                                        found = true;
                                        break;
                                    }
                                }
                            }

                            if (!found && ImageList.isImageURL(url) && !ImageList.getInstance().isFailedURL(new URL(url))) {
                                ImageList.getInstance().fetchImageInfo(url, new ImageList.OnImageInfoListener() {
                                    @Override
                                    public void onImageInfo(ImageURLInfo info) {
                                        if(info != null) {
                                            ObjectNode properties = new ObjectMapper().createObjectNode();

                                            properties.put("url", info.original_url);
                                            properties.put("thumbnail", info.thumbnail);
                                            if(info.title != null)
                                                properties.put("name", info.title);
                                            if(info.description != null)
                                                properties.put("description", info.description);

                                            Uri uri = Uri.parse(info.thumbnail);
                                            if (uri.getLastPathSegment().contains(".")) {
                                                String extension = uri.getLastPathSegment().substring(uri.getLastPathSegment().indexOf(".") + 1).toLowerCase();
                                                properties.put("mime_type", "image/" + extension);
                                            } else {
                                                properties.put("mime_type", "image/image");
                                            }

                                            insertEntity(messageAdapter, event, properties, backlog || !ready);
                                            try {
                                                URL u = new URL(info.thumbnail);
                                                int width = getActivity().getWindowManager().getDefaultDisplay().getWidth();
                                                if(getActivity().getWindowManager().getDefaultDisplay().getHeight() < width)
                                                    width = getActivity().getWindowManager().getDefaultDisplay().getHeight();
                                                width /= 2;
                                                ImageList.getInstance().fetchImage(u, width, null, 0);
                                            } catch (Exception e) {
                                                e.printStackTrace();
                                            }
                                        }
                                    }
                                });
                            }
                        }
                    }
                }

                if (!backlog)
                    messageAdapter.notifyDataSetChanged();

                long time = (System.currentTimeMillis() - start);
                if (avgInsertTime == 0)
                    avgInsertTime = time;
                avgInsertTime += time;
                avgInsertTime /= 2.0;
                //Log.i("IRCCloud", "Average insert time: " + avgInsertTime);
                if (!backlog && buffer.getScrolledUp() && !event.self && event.isImportant(type)) {
                    if(event.row_type != ROW_THUMBNAIL && event.row_type != ROW_FILE) {
                        if (newMsgTime == 0)
                            newMsgTime = System.currentTimeMillis();
                        newMsgs++;
                        if (event.highlight && !pref_muted)
                            newHighlights++;
                        update_unread();
                        messageAdapter.insertLastSeenEIDMarker();
                    }
                    messageAdapter.notifyDataSetChanged();
                }
                if (!backlog && !buffer.getScrolledUp()) {
                    getListView().setSelection(messageAdapter.getCount() - 1);
                    if (tapTimer != null) {
                        tapTimer.schedule(new TimerTask() {
                            @Override
                            public void run() {
                                runOnUiThread(new Runnable() {
                                    @Override
                                    public void run() {
                                        try {
                                            getListView().setSelection(messageAdapter.getCount() - 1);
                                        } catch (Exception e) {
                                            //List view isn't ready yet
                                        }
                                    }
                                });
                            }
                        }, 200);
                    }
                }

                if (!backlog && event.highlight && !getActivity().getSharedPreferences("prefs", 0).getBoolean("mentionTip", false)) {
                    Toast.makeText(getActivity(), "Double-tap a message to quickly reply to the sender", Toast.LENGTH_LONG).show();
                    SharedPreferences.Editor editor = getActivity().getSharedPreferences("prefs", 0).edit();
                    editor.putBoolean("mentionTip", true);
                    editor.apply();
                }
                if (!backlog) {
                    int markerPos = messageAdapter.getLastSeenEIDPosition();
                    if (markerPos > 0 && getListView().getFirstVisiblePosition() > markerPos) {
                        unreadTopLabel.setText((getListView().getFirstVisiblePosition() - markerPos) + " unread messages");
                    }
                }

                if(!pref_disableInlineFiles && event.entities != null && event.entities.get("files") != null) {
                    JsonNode files = event.entities.get("files");

                    for(int i = 0; i < files.size(); i++) {
                        JsonNode entity = files.get(i);
                        String fileID = entity.get("id").asText();
                        if(!hiddenFileIDs.contains(fileID)) {
                            JsonNode properties = filePropsCache.get(fileID);
                            if(BuildConfig.MOCK_DATA) {
                                JSONObject o = new JSONObject(entity.toString());
                                o.remove("id");
                                o.put("thumbnail", "https://www.irccloud.com/static/test/" + o.getString("filename"));
                                properties = new ObjectMapper().readValue(o.toString(), JsonNode.class);;
                            }

                            if (properties != null) {
                                insertEntity(messageAdapter, event, properties, backlog);
                            } else {
                                new FilePropsTask(messageAdapter, fileID, event).connect();
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // TODO Auto-generated catch block
                NetworkConnection.printStackTraceToCrashlytics(e);
            }
        }
    }

    private synchronized void insertEntity(final MessageAdapter messageAdapter, Event parent, JsonNode properties, boolean backlog) {
        Event e = new Event();
        e.cid = parent.cid;
        e.bid = parent.bid;
        e.eid = parent.eid;
        e.from = parent.from;
        e.nick = parent.nick;
        e.self = parent.self;
        e.from_mode = parent.from_mode;
        e.from_realname = parent.from_realname;
        e.hostmask = parent.hostmask;
        e.parent_eid = parent.eid;
        e.reqid = parent.reqid;
        e.avatar = parent.avatar;
        e.avatar_url = parent.avatar_url;
        e.linkify = true;

        if(properties.get("mime_type").asText().startsWith("image/"))
            e.row_type = ROW_THUMBNAIL;
        else
            e.row_type = ROW_FILE;

        if(parent.type.equals("buffer_me_msg"))
            e.type = "buffer_msg";
        else
            e.type = parent.type;
        e.bg_color = (e.self && e.type.equals("buffer_msg")) ? ColorScheme.getInstance().selfBackgroundColor : parent.bg_color;
        e.entities = properties;

        if(properties.has("size")) {
            int size = properties.get("size").asInt();
            if (size < 1024) {
                e.msg = size + " B";
            } else {
                int exp = (int) (Math.log(size) / Math.log(1024));
                e.msg = String.format("%.1f ", size / Math.pow(1024, exp)) + ("KMGTPE".charAt(exp - 1)) + "B";
            }

            if (e.row_type == ROW_THUMBNAIL)
                e.msg += " • ";
            else
                e.msg += "\n";

            e.msg += properties.get("mime_type").asText();
        } else if(properties.has("description")) {
            e.msg = properties.get("description").asText();
        }
        if(e.msg != null)
            e.html = e.msg = TextUtils.htmlEncode(e.msg);
        e.ready_for_display = true;
        messageAdapter.insertBelow(parent.eid, e, backlog);
        if(!backlog) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (buffer != null && !buffer.getScrolledUp()) {
                        getListView().setSelection(messageAdapter.getCount() - 1);
                        if (tapTimer != null) {
                            tapTimer.schedule(new TimerTask() {
                                @Override
                                public void run() {
                                    runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            try {
                                                getListView().setSelection(messageAdapter.getCount() - 1);
                                            } catch (Exception e) {
                                                //List view isn't ready yet
                                            }
                                        }
                                    });
                                }
                            }, 200);
                        }
                    }
                    messageAdapter.notifyDataSetChanged();
                }
            });
        }
    }

    private class OnItemLongClickListener implements View.OnLongClickListener {
        private Event event;

        OnItemLongClickListener(Event e) { event = e; }

        @Override
        public boolean onLongClick(View view) {
            try {
                longPressOverride = mListener.onMessageLongClicked(event);
                return longPressOverride;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return false;
        }
    }

    private class OnItemClickListener implements OnClickListener {
        private Event event;

        OnItemClickListener(Event e) {
            event = e;
        }

        @Override
        public void onClick(View arg0) {
            longPressOverride = false;

            if (event == null)
                return;

            if(tapTimer == null)
                tapTimer = new Timer("message-tap-timer");

            if (adapter != null) {
                if (tapTimerTask != null) {
                    tapTimerTask.cancel();
                    tapTimerTask = null;
                    mListener.onMessageDoubleClicked(event);
                } else {
                    tapTimerTask = new TimerTask() {
                        @Override
                        public void run() {
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    if (adapter != null) {
                                        if (event.type.equals("channel_invite")) {
                                            if(mListener != null)
                                                mListener.promptToJoin(event.old_nick, null, server);
                                        } else if (event.type.equals("callerid")) {
                                            conn.say(buffer.getCid(), null, "/accept " + event.from, null);
                                            Buffer b = BuffersList.getInstance().getBufferByName(buffer.getCid(), event.from);
                                            if (b != null) {
                                                mListener.onBufferSelected(b.getBid());
                                            } else {
                                                conn.say(buffer.getCid(), null, "/query " + event.from, null);
                                            }
                                        } else if (event.row_type == ROW_THUMBNAIL) {
                                            try {
                                                String uri;
                                                if(event.entities.has("id"))
                                                    uri = UriTemplate.fromTemplate(NetworkConnection.file_uri_template).set("id", event.entities.get("id").asText()).set("name", event.entities.get("name").asText()).expand();
                                                else
                                                    uri = event.entities.get("url").asText();

                                                if (event.entities == null || !event.entities.has("mime_type") || !event.entities.get("mime_type").asText().equalsIgnoreCase("image/heic")) {
                                                    if (PreferenceManager.getDefaultSharedPreferences(IRCCloudApplication.getInstance().getApplicationContext()).getBoolean("imageviewer", true)) {
                                                        if (uri.toLowerCase().startsWith("http://"))
                                                            uri = IRCCloudApplication.getInstance().getApplicationContext().getResources().getString(R.string.IMAGE_SCHEME) + "://" + uri.substring(7);
                                                        else if (uri.toLowerCase().startsWith("https://"))
                                                            uri = IRCCloudApplication.getInstance().getApplicationContext().getResources().getString(R.string.IMAGE_SCHEME_SECURE) + "://" + uri.substring(8);
                                                    }
                                                }
                                                IRCCloudLinkMovementMethod.launchURI(Uri.parse(uri), getActivity());
                                            } catch (Exception ex) {
                                                ex.printStackTrace();
                                            }
                                        } else if (event.row_type == ROW_FILE) {
                                            try {
                                                IRCCloudLinkMovementMethod.launchURI(Uri.parse(UriTemplate.fromTemplate(NetworkConnection.file_uri_template).set("id", event.entities.get("id").asText()).set("name", event.entities.get("name").asText()).expand()), getActivity());
                                            } catch (ActivityNotFoundException ex) {
                                                Toast.makeText(getActivity(), "Unable to find an application to handle this URL scheme", Toast.LENGTH_SHORT).show();
                                            } catch (Exception ex) {
                                                ex.printStackTrace();
                                            }
                                        } else if (event.failed) {
                                            if(mListener != null)
                                                mListener.onFailedMessageClicked(event);
                                        } else {
                                            long group = event.group_eid;
                                            if (expandedSectionEids.contains(group))
                                                expandedSectionEids.remove(group);
                                            else if (event.eid != group)
                                                expandedSectionEids.add(group);
                                            if (event.eid != event.group_eid) {
                                                adapter.clearLastSeenEIDMarker();
                                                if (refreshTask != null)
                                                    refreshTask.cancel(true);
                                                refreshTask = new RefreshTask();
                                                refreshTask.execute((Void) null);
                                            }
                                        }
                                    }
                                }
                            });
                            tapTimerTask = null;
                        }
                    };
                    tapTimer.schedule(tapTimerTask, 300);
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    public void onResume() {
        super.onResume();
        getListView().setOnScrollListener(mOnScrollListener);
        update_global_msg();
        if (buffer != null && adapter != null) {
            if(buffer.getUnread() == 0 && !buffer.getScrolledUp())
                adapter.clearLastSeenEIDMarker();
            adapter.notifyDataSetChanged();
        }
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        getListView().setAdapter(null);
    }

    @Override
    public void onStop() {
        if(headerViewContainer != null)
            headerViewContainer.setVisibility(View.GONE);
        if(footerViewContainer != null)
            footerViewContainer.setVisibility(View.GONE);
        super.onStop();
    }

    @Override
    public void onStart() {
        if(headerViewContainer != null)
            headerViewContainer.setVisibility(View.VISIBLE);
        if(footerViewContainer != null)
            footerViewContainer.setVisibility(View.VISIBLE);
        super.onStart();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (tapTimer != null) {
            tapTimer.cancel();
            tapTimer = null;
        }
        if(conn != null)
            conn.removeHandler(this);
        mListener = null;
        heartbeatTask = null;
    }

    private class HeartbeatTask extends AsyncTaskEx<Void, Void, Void> {
        Buffer b;

        public HeartbeatTask() {
            b = buffer;
            /*if(buffer != null)
                Log.d("IRCCloud", "Heartbeat task created. Ready: " + ready + " BID: " + buffer.getBid());
            Thread.dumpStack();*/
        }

        @Override
        protected Void doInBackground(Void... params) {
            try {
                Thread.sleep(250);
            } catch (InterruptedException e) {
            }

            if (isCancelled() || !conn.ready || conn.getState() != NetworkConnection.STATE_CONNECTED || b == null || !ready || requestingBacklog || adapter.getCount() == 0)
                return null;

            if (getActivity() != null) {
                try {
                    DrawerLayout drawerLayout = getActivity().findViewById(R.id.drawerLayout);

                    if (drawerLayout != null && (drawerLayout.isDrawerOpen(Gravity.LEFT) || drawerLayout.isDrawerOpen(Gravity.RIGHT)))
                        return null;
                } catch (Exception e) {
                }
            }

            if (unreadTopView.getVisibility() == View.VISIBLE || unreadBottomView.getVisibility() == View.VISIBLE)
                return null;

            try {
                Long eid = EventsList.getInstance().lastEidForBuffer(b.getBid());

                if (eid >= b.getLast_seen_eid() && conn != null && conn.getState() == NetworkConnection.STATE_CONNECTED) {
                    if (getActivity() != null && getActivity().getIntent() != null)
                        getActivity().getIntent().putExtra("last_seen_eid", eid);
                    NetworkConnection.getInstance().heartbeat(b.getCid(), b.getBid(), eid, null);
                    b.setLast_seen_eid(eid);
                    b.setUnread(0);
                    b.setHighlights(0);
                    //Log.e("IRCCloud", "Heartbeat: " + buffer.name + ": " + events.get(events.lastKey()).msg);
                }
            } catch (Exception e) {
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            if (!isCancelled())
                heartbeatTask = null;
        }
    }

    private class FilePropsTask extends JSONFetcher {
        private Event event;
        private String fileID;
        private MessageAdapter adapter;

        FilePropsTask(MessageAdapter adapter, String fileID, Event event) throws Exception {
            super(new URL("https://" + NetworkConnection.IRCCLOUD_HOST + "/file/json/" + fileID));
            this.fileID = fileID;
            this.event = event;
            this.adapter = adapter;
        }

        @Override
        protected void onJSONParsed(final JsonNode result) {
            if(result != null) {
                filePropsCache.put(fileID, result);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        insertEntity(adapter, event, result, false);
                    }
                });
            }
        }
    }

    private class RefreshTask extends AsyncTaskEx<Void, Void, Void> {
        private MessageAdapter adapter;

        TreeMap<Long, Event> events;
        Buffer buffer;
        int oldPosition = -1;
        int topOffset = -1;

        RefreshTask() {
        }

        @Override
        protected void onPreExecute() {
            //Debug.startMethodTracing("refresh");
            try {
                oldPosition = getListView().getFirstVisiblePosition();
                View v = getListView().getChildAt(0);
                topOffset = (v == null) ? 0 : v.getTop();
                buffer = MessageViewFragment.this.buffer;
            } catch (IllegalStateException e) {
                //The list view isn't on screen anymore
                cancel(true);
                refreshTask = null;
            }
        }

        @SuppressWarnings("unchecked")
        @Override
        protected Void doInBackground(Void... params) {
            TreeMap<Long, Event> evs = null;
            long time = System.currentTimeMillis();
            if (buffer != null)
                evs = EventsList.getInstance().getEventsForBuffer(buffer.getBid());

            Log.i("IRCCloud", "Loaded data in " + (System.currentTimeMillis() - time) + "ms");
            if (!isCancelled() && evs != null && evs.size() > 0) {
                try {
                    events = (TreeMap<Long, Event>) evs.clone();
                } catch (Exception e) {
                    NetworkConnection.printStackTraceToCrashlytics(e);
                    return null;
                }
                if (isCancelled())
                    return null;

                if (events != null) {
                    try {
                        if (events.size() > 0 && MessageViewFragment.this.adapter != null && MessageViewFragment.this.adapter.data.size() > 0 && earliest_eid > events.firstKey()) {
                            if (oldPosition > 0 && oldPosition == MessageViewFragment.this.adapter.data.size())
                                oldPosition--;
                            Event e = MessageViewFragment.this.adapter.data.get(oldPosition);
                            if (e != null)
                                backlog_eid = e.group_eid - 1;
                            else
                                backlog_eid = -1;
                            if (backlog_eid < 0) {
                                backlog_eid = MessageViewFragment.this.adapter.getItemId(oldPosition) - 1;
                            }
                            Event backlogMarker = new Event();
                            backlogMarker.eid = backlog_eid;
                            backlogMarker.type = TYPE_BACKLOGMARKER;
                            backlogMarker.row_type = ROW_BACKLOGMARKER;
                            backlogMarker.html = "__backlog__";
                            backlogMarker.bg_color = colorScheme.contentBackgroundColor;
                            events.put(backlog_eid, backlogMarker);
                        }
                        adapter = new MessageAdapter(MessageViewFragment.this, events.size());
                        refresh(adapter, events);
                    } catch (IllegalStateException e) {
                        //The list view doesn't exist yet
                        NetworkConnection.printStackTraceToCrashlytics(e);
                        Log.e("IRCCloud", "Tried to refresh the message list, but it didn't exist.");
                    } catch (Exception e) {
                        NetworkConnection.printStackTraceToCrashlytics(e);
                        return null;
                    }
                }
            } else if (buffer != null && buffer.getMin_eid() > 0 && conn.ready && conn.getState() == NetworkConnection.STATE_CONNECTED && !isCancelled() && !(msgid != null && msgids.containsKey(msgid))) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        earliest_eid = 0;
                        headerView.setVisibility(View.GONE);
                        backlogFailed.setVisibility(View.GONE);
                        loadBacklogButton.setVisibility(View.VISIBLE);
                        adapter = new MessageAdapter(MessageViewFragment.this, 0);
                        setListAdapter(adapter);
                        MessageViewFragment.this.adapter = adapter;
                    }
                });
            } else {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        headerView.setVisibility(View.GONE);
                        backlogFailed.setVisibility(View.GONE);
                        loadBacklogButton.setVisibility(View.GONE);
                        adapter = new MessageAdapter(MessageViewFragment.this, 0);
                        setListAdapter(adapter);
                        MessageViewFragment.this.adapter = adapter;
                    }
                });
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            if (!isCancelled() && adapter != null) {
                try {
                    avatarContainer.setVisibility(View.GONE);
                    avatar.setTag(null);
                    if (adapter.getLastSeenEIDPosition() == 0)
                        headerViewContainer.setPadding(0,(int)getSafeResources().getDimension(R.dimen.top_bar_height),0,0);
                    else
                        headerViewContainer.setPadding(0,0,0,0);
                    setListAdapter(adapter);
                    MessageViewFragment.this.adapter = adapter;
                    if (events != null && events.size() > 0) {
                        int markerPos = adapter.getBacklogMarkerPosition();
                        if (markerPos != -1 && requestingBacklog)
                            getListView().setSelectionFromTop(oldPosition + markerPos + 1, headerViewContainer.getHeight());
                        else if (!buffer.getScrolledUp())
                            getListView().setSelection(adapter.getCount() - 1);
                        else {
                            getListView().setSelectionFromTop(buffer.getScrollPosition(), buffer.getScrollPositionOffset());

                            if (adapter.getLastSeenEIDPosition() > buffer.getScrollPosition()) {
                                newMsgs = 0;
                                newHighlights = 0;

                                for (int i = adapter.data.size() - 1; i >= 0; i--) {
                                    Event e = adapter.data.get(i);
                                    if (e.eid <= buffer.getLast_seen_eid())
                                        break;

                                    if (e.isImportant(buffer.getType())) {
                                        if (e.highlight && !pref_muted)
                                            newHighlights++;
                                        else
                                            newMsgs++;
                                    }
                                }
                            }

                            update_unread();
                        }
                    } else {
                        earliest_eid = 0;
                    }
                } catch (IllegalStateException e) {
                    //The list view isn't on screen anymore
                    NetworkConnection.printStackTraceToCrashlytics(e);
                }
                refreshTask = null;
                requestingBacklog = false;
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        if(getListAdapter() == RefreshTask.this.adapter) {
                            try {
                                update_top_unread(getListView().getFirstVisiblePosition());
                            } catch (IllegalStateException e) {
                                //List view not ready yet
                            }
                            if (server != null)
                                update_status(server.getStatus(), server.getFail_info());
                            if (mListener != null && !ready)
                                mListener.onMessageViewReady();
                            ready = true;
                            try {
                                ListView v = getListView();
                                if (fetch_if_needed && ready && !requestingBacklog && buffer.getMin_eid() > 0 && earliest_eid > buffer.getMin_eid() && conn.ready && conn.getState() == NetworkConnection.STATE_CONNECTED && !(msgid != null && msgids.containsKey(msgid)) && v.getFirstVisiblePosition() == 0) {
                                    backlogFailed.setVisibility(View.GONE);
                                    loadBacklogButton.setVisibility(View.GONE);
                                    headerView.setVisibility(View.VISIBLE);
                                    requestingBacklog = true;
                                    fetch_if_needed = false;
                                    buffer.setDeferred(0);
                                    NetworkConnection.getInstance().request_backlog(buffer.getCid(), buffer.getBid(), earliest_eid);
                                }
                                mOnScrollListener.onScroll(v, v.getFirstVisiblePosition(), v.getLastVisiblePosition() - v.getFirstVisiblePosition(), adapter.getCount());
                            } catch (Exception e) {
                            }
                        }
                    }
                }, 250);
                //Debug.stopMethodTracing();
            }
        }
    }

    private class FormatTask extends AsyncTaskEx<Void, Void, Void> {
        private MessageAdapter adapter;
        private TreeMap<Long, Event> events;

        public FormatTask(MessageAdapter adapter, TreeMap<Long, Event> events) {
            this.adapter = adapter;
            this.events = events;
        }

        @Override
        protected Void doInBackground(Void... params) {
            LinkifyTask linkifyTask = new LinkifyTask(null);
            synchronized (this.adapter) {
                if (events != null) {
                    for (Event e : events.values()) {
                        if(isCancelled())
                            break;
                        if (e != null) {
                            this.adapter.format(e, true);
                            linkifyTask.e = e;
                            linkifyTask.doInBackground((Void)null);
                        }
                    }
                }
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            if(!isCancelled() && this.adapter != null)
                this.adapter.notifyDataSetChanged();
            if(mainFormatTask == this)
                mainFormatTask = null;
        }
    }

    private class LinkifyTask extends AsyncTaskEx<Void, Void, Void> {
        public Event e;
        private boolean notify = false;

        public LinkifyTask(Event event) {
            e = event;
        }

        @Override
        protected Void doInBackground(Void... params) {
            if(isCancelled())
                return null;

            try {
                if (e != null) {
                    synchronized (e) {
                        if (!e.linkified) {
                            if (e.formatted != null && e.linkify) {
                                ColorFormatter.detectLinks((Spannable) e.formatted);
                                if(isCancelled())
                                    return null;
                                if(pref_chatOneLine || e.type.equals("buffer_me_msg")) {
                                    String from = e.from;
                                    if (from == null)
                                        from = e.nick;
                                    if (from != null) {
                                        Matcher m = Pattern.compile("(^|\\s)" + Pattern.quote(from) + "($|\\s)").matcher(e.formatted);
                                        if (m.find() && m.start() < 4) {
                                            ClickableSpan[] spans = e.formatted.getSpans(m.start(), m.end(), ClickableSpan.class);
                                            if (spans != null) {
                                                for (ClickableSpan span : spans)
                                                    ((Spannable) e.formatted).removeSpan(span);
                                            }
                                        }
                                    }
                                }
                                notify = true;
                            }

                            if (e.contentDescription != null && e.linkify) {
                                ColorFormatter.detectLinks((Spannable) e.contentDescription);
                                notify = true;
                            }

                            if (e.formatted_realname != null) {
                                ColorFormatter.detectLinks((Spannable) e.formatted_realname);
                                notify = true;
                            }

                        }
                        e.linkified = true;

                        if (e.formatted != null && !isCancelled()) {
                            synchronized (e.formatted) {
                                if (e.entities != null && NetworkConnection.file_uri_template != null && e.entities.has("files")) {
                                    UriTemplate template = UriTemplate.fromTemplate(NetworkConnection.file_uri_template);
                                    String file_url_prefix = template.expand().toLowerCase();
                                    URLSpan[] urls = e.formatted.getSpans(0, e.formatted.length(), URLSpan.class);
                                    for (URLSpan url : urls) {
                                        for (JsonNode file : e.entities.get("files")) {
                                            if (url.getURL().toLowerCase().startsWith(file_url_prefix) && url.getURL().contains(file.get("id").asText())) {
                                                IRCCloudLinkMovementMethod.addFileID(url.getURL(), file.get("id").asText());
                                            }
                                        }
                                    }
                                }

                                if (precomputedTextParams != null)
                                    e.formatted = PrecomputedTextCompat.create(e.formatted, precomputedTextParams);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                IRCCloudLog.LogException(e);
                NetworkConnection.printStackTraceToCrashlytics(e);
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            if(!isCancelled()) {
                if(adapter != null && notify)
                    adapter.notifyDataSetChanged();
                linkifyTasks.remove(e.eid);
            }
        }
    }

    private synchronized void refresh(MessageAdapter adapter, TreeMap<Long, Event> events) {
        ImageList.getInstance().clear();
        IRCCloudLinkMovementMethod.clearFileIDs();
        msgids.clear();
        for(LinkifyTask t : linkifyTasks.values())
            t.cancel(true);
        linkifyTasks.clear();
        earliest_eid = 0;
        pref_24hr = false;
        pref_seconds = false;
        pref_trackUnread = true;
        pref_timeLeft = false;
        pref_nickColors = false;
        pref_hideJoinPart = false;
        pref_expandJoinPart = false;
        pref_avatarsOff = false;
        pref_chatOneLine = false;
        pref_norealname = false;
        pref_compact = false;
        pref_disableLargeEmoji = false;
        pref_disableInlineFiles = false;
        pref_disableCodeSpan = false;
        pref_disableCodeBlock = false;
        pref_disableQuote = false;
        pref_inlineImages = false;
        pref_avatarImages = false;
        pref_mentionColors = false;
        pref_muted = false;
        pref_noColor = false;
        buffer_usermask = null;
        if (NetworkConnection.getInstance().getUserInfo() != null && NetworkConnection.getInstance().getUserInfo().prefs != null) {
            try {
                JSONObject prefs = NetworkConnection.getInstance().getUserInfo().prefs;
                pref_compact = (prefs.has("ascii-compact") && prefs.get("ascii-compact") instanceof Boolean && prefs.getBoolean("ascii-compact"));
                pref_24hr = (prefs.has("time-24hr") && prefs.get("time-24hr") instanceof Boolean && prefs.getBoolean("time-24hr"));
                pref_seconds = (prefs.has("time-seconds") && prefs.get("time-seconds") instanceof Boolean && prefs.getBoolean("time-seconds"));
                pref_timeLeft = !PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("time-left", true);
                pref_nickColors = (prefs.has("nick-colors") && prefs.get("nick-colors") instanceof Boolean && prefs.getBoolean("nick-colors"));
                pref_avatarsOff = !PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("avatars-off", true);
                pref_avatarImages = PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("avatar-images", false);
                pref_chatOneLine = !PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("chat-oneline", true);
                pref_norealname = !PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("chat-norealname", true);
                pref_disableLargeEmoji = !PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("emoji-nobig", true);
                pref_disableCodeSpan = (prefs.has("chat-nocodespan") && prefs.get("chat-nocodespan") instanceof Boolean && prefs.getBoolean("chat-nocodespan"));
                pref_disableCodeBlock = (prefs.has("chat-nocodeblock") && prefs.get("chat-nocodeblock") instanceof Boolean && prefs.getBoolean("chat-nocodeblock"));
                pref_disableQuote = (prefs.has("chat-noquote") && prefs.get("chat-noquote") instanceof Boolean && prefs.getBoolean("chat-noquote"));
                pref_mentionColors = (prefs.has("mention-colors") && prefs.get("mention-colors") instanceof Boolean && prefs.getBoolean("mention-colors"));

                if(prefs.has("channel-disableTrackUnread")) {
                    JSONObject disabledMap = prefs.getJSONObject("channel-disableTrackUnread");
                    if (disabledMap.has(String.valueOf(buffer.getBid())) && disabledMap.getBoolean(String.valueOf(buffer.getBid()))) {
                        pref_trackUnread = false;
                    }
                }

                if(prefs.has("hideJoinPart") && prefs.get("hideJoinPart") instanceof Boolean && prefs.getBoolean("hideJoinPart")) {
                    JSONObject showMap = null;
                    if (buffer.isChannel()) {
                        if (prefs.has("channel-showJoinPart"))
                            showMap = prefs.getJSONObject("channel-showJoinPart");
                    } else {
                        if (prefs.has("buffer-showJoinPart"))
                            showMap = prefs.getJSONObject("buffer-showJoinPart");
                    }

                    pref_hideJoinPart = !(showMap != null && showMap.has(String.valueOf(buffer.getBid())) && showMap.getBoolean(String.valueOf(buffer.getBid())));
                } else {
                    JSONObject hiddenMap = null;
                    if (buffer.isChannel()) {
                        if (prefs.has("channel-hideJoinPart"))
                            hiddenMap = prefs.getJSONObject("channel-hideJoinPart");
                    } else {
                        if (prefs.has("buffer-hideJoinPart"))
                            hiddenMap = prefs.getJSONObject("buffer-hideJoinPart");
                    }

                    pref_hideJoinPart = (hiddenMap != null && hiddenMap.has(String.valueOf(buffer.getBid())) && hiddenMap.getBoolean(String.valueOf(buffer.getBid())));
                }

                JSONObject expandMap = null;
                JSONObject collapseMap = null;
                if (buffer.isChannel()) {
                    if (prefs.has("channel-expandJoinPart"))
                        expandMap = prefs.getJSONObject("channel-expandJoinPart");
                    if (prefs.has("channel-collapseJoinPart"))
                        collapseMap = prefs.getJSONObject("channel-collapseJoinPart");
                } else if (buffer.isConsole()) {
                    if (prefs.has("buffer-expandDisco"))
                        expandMap = prefs.getJSONObject("buffer-expandDisco");
                } else {
                    if (prefs.has("buffer-expandJoinPart"))
                        expandMap = prefs.getJSONObject("buffer-expandJoinPart");
                    if (prefs.has("buffer-collapseJoinPart"))
                        collapseMap = prefs.getJSONObject("buffer-collapseJoinPart");
                }

                pref_expandJoinPart = (prefs.has("expandJoinPart") && prefs.get("expandJoinPart") instanceof Boolean && prefs.getBoolean("expandJoinPart"));
                if(pref_expandJoinPart) {
                    pref_expandJoinPart = !(collapseMap != null && collapseMap.has(String.valueOf(buffer.getBid())) && collapseMap.getBoolean(String.valueOf(buffer.getBid())));
                } else {
                    pref_expandJoinPart = (expandMap != null && expandMap.has(String.valueOf(buffer.getBid())) && expandMap.getBoolean(String.valueOf(buffer.getBid())));
                }

                if(prefs.has("inlineimages") && prefs.get("inlineimages") instanceof Boolean && prefs.getBoolean("inlineimages")) {
                    JSONObject inlineImagesMap = null;
                    if (buffer.isChannel()) {
                        if (prefs.has("channel-inlineimages-disable"))
                            inlineImagesMap = prefs.getJSONObject("channel-inlineimages-disable");
                    } else {
                        if (prefs.has("buffer-inlineimages-disable"))
                            inlineImagesMap = prefs.getJSONObject("buffer-inlineimages-disable");
                    }

                    pref_inlineImages = !(inlineImagesMap != null && inlineImagesMap.has(String.valueOf(buffer.getBid())) && inlineImagesMap.getBoolean(String.valueOf(buffer.getBid())));
                } else {
                    JSONObject inlineImagesMap = null;
                    if (buffer.isChannel()) {
                        if (prefs.has("channel-inlineimages"))
                            inlineImagesMap = prefs.getJSONObject("channel-inlineimages");
                    } else {
                        if (prefs.has("buffer-inlineimages"))
                            inlineImagesMap = prefs.getJSONObject("buffer-inlineimages");
                    }

                    pref_inlineImages = (inlineImagesMap != null && inlineImagesMap.has(String.valueOf(buffer.getBid())) && inlineImagesMap.getBoolean(String.valueOf(buffer.getBid())));
                }

                //boolean isLowRamDevice = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && getActivity() != null && ((ActivityManager)(getActivity().getSystemService(Context.ACTIVITY_SERVICE))).isLowRamDevice();
                if(/*isLowRamDevice || */(!PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("files-usemobiledata", false) && !conn.isWifi())) {
                    pref_disableInlineFiles = true;
                    pref_inlineImages = false;
                }

                JSONObject replyCollapseMap = null;
                if (buffer.isChannel()) {
                    if (prefs.has("channel-reply-collapse"))
                        replyCollapseMap = prefs.getJSONObject("channel-reply-collapse");
                } else {
                    if (prefs.has("buffer-reply-collapse"))
                        replyCollapseMap = prefs.getJSONObject("buffer-reply-collapse");
                }

                pref_replyCollapse = (prefs.has("reply-collapse") && prefs.get("reply-collapse") instanceof Boolean && prefs.getBoolean("reply-collapse")) || (replyCollapseMap != null && replyCollapseMap.has(String.valueOf(buffer.getBid())) && replyCollapseMap.getBoolean(String.valueOf(buffer.getBid())));

                String pref_type = buffer.isChannel() ? "channel" : "buffer";
                pref_muted = (prefs.has("notifications-mute") && prefs.get("notifications-mute") instanceof Boolean && prefs.getBoolean("notifications-mute"));
                if (prefs.has(pref_type + "-notifications-mute")) {
                    JSONObject notifyMuteMap = prefs.getJSONObject(pref_type + "-notifications-mute");
                    if (notifyMuteMap.has(String.valueOf(buffer.getBid())) && notifyMuteMap.getBoolean(String.valueOf(buffer.getBid())))
                        pref_muted = true;
                }
                if (prefs.has(pref_type + "-notifications-mute-disable")) {
                    JSONObject notifyMuteMap = prefs.getJSONObject(pref_type + "-notifications-mute-disable");
                    if (notifyMuteMap.has(String.valueOf(buffer.getBid())) && notifyMuteMap.getBoolean(String.valueOf(buffer.getBid())))
                        pref_muted = false;
                }

                pref_noColor = (prefs.has("chat-nocolor") && prefs.get("chat-nocolor") instanceof Boolean && prefs.getBoolean("chat-nocolor"));
                if (prefs.has(pref_type + "-chat-nocolor")) {
                    JSONObject noColorMap = prefs.getJSONObject(pref_type + "-chat-nocolor");
                    if (noColorMap.has(String.valueOf(buffer.getBid())) && noColorMap.getBoolean(String.valueOf(buffer.getBid())))
                        pref_noColor = true;
                }
                if (prefs.has(pref_type + "-chat-color")) {
                    JSONObject colorMap = prefs.getJSONObject(pref_type + "-chat-color");
                    if (colorMap.has(String.valueOf(buffer.getBid())) && colorMap.getBoolean(String.valueOf(buffer.getBid())))
                        pref_noColor = false;
                }

                pref_disableInlineFiles = (prefs.has("files-disableinline") && prefs.get("files-disableinline") instanceof Boolean && prefs.getBoolean("files-disableinline"));
                if (prefs.has(pref_type + "-files-disableinline")) {
                    JSONObject noInlineMap = prefs.getJSONObject(pref_type + "-files-disableinline");
                    if (noInlineMap.has(String.valueOf(buffer.getBid())) && noInlineMap.getBoolean(String.valueOf(buffer.getBid())))
                        pref_disableInlineFiles = true;
                }
                if (prefs.has(pref_type + "-files-enableinline")) {
                    JSONObject inlineMap = prefs.getJSONObject(pref_type + "-files-enableinline");
                    if (inlineMap.has(String.valueOf(buffer.getBid())) && inlineMap.getBoolean(String.valueOf(buffer.getBid())))
                        pref_disableInlineFiles = false;
                }

                if(msgid != null && msgid.length() > 0)
                    pref_replyCollapse = false;
            } catch (JSONException e1) {
                NetworkConnection.printStackTraceToCrashlytics(e1);
            }
        }

        if(BuildConfig.MOCK_DATA) {
            pref_24hr = false;
            pref_seconds = false;
            pref_trackUnread = true;
            pref_timeLeft = false;
            pref_nickColors = true;
            pref_hideJoinPart = false;
            pref_expandJoinPart = false;
            pref_avatarsOff = false;
            pref_chatOneLine = false;
            pref_norealname = false;
            pref_compact = false;
            pref_disableLargeEmoji = false;
            pref_disableInlineFiles = false;
            pref_disableCodeSpan = false;
            pref_disableCodeBlock = false;
            pref_disableQuote = false;
            pref_inlineImages = true;
            pref_avatarImages = true;
            pref_mentionColors = true;
            pref_replyCollapse = false;
        }
        synchronized (adapterLock) {
            if (getActivity() != null)
                textSize = PreferenceManager.getDefaultSharedPreferences(getActivity()).getInt("textSize", getActivity().getResources().getInteger(R.integer.default_text_size));
            timestamp_width = -1;
            if (conn.getReconnectTimestamp() == 0)
                conn.cancel_idle_timer(); //This may take a while...
            collapsedEvents.clear();
            collapsedEvents.noColor = pref_noColor;
            currentCollapsedEid = -1;
            lastCollapsedDay = -1;

            if (!requestingBacklog && (events == null || events.size() < 50))
                fetch_if_needed = true;

            if (events == null || (events.size() == 0 && buffer.getMin_eid() > 0)) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        headerView.setVisibility(View.GONE);
                        backlogFailed.setVisibility(View.GONE);
                        loadBacklogButton.setVisibility(View.GONE);
                    }
                });
            } else if (events.size() > 0) {
                Trace trace = null;
                try {
                    trace = FirebasePerformance.getInstance().newTrace("loadBacklog");
                    trace.start();
                } catch (IllegalStateException e) {

                }
                if (server != null) {
                    ignore = server.ignores;
                } else {
                    ignore = new Ignore();
                }
                collapsedEvents.setServer(server);
                earliest_eid = events.firstKey();
                if (events.size() > 0) {
                    avgInsertTime = 0;
                    //Debug.startMethodTracing("refresh");
                    long start = System.currentTimeMillis();
                    Iterator<Event> i = events.values().iterator();
                    Event next = i.next();
                    Calendar calendar = Calendar.getInstance();
                    while (next != null) {
                        Event e = next;
                        next = i.hasNext() ? i.next() : null;
                        String type = (next == null) ? "" : next.type;

                        e.reply_count = 0;
                        if (next != null && currentCollapsedEid != -1 && !expandedSectionEids.contains(currentCollapsedEid) && (type.equalsIgnoreCase("joined_channel") || type.equalsIgnoreCase("parted_channel") || type.equalsIgnoreCase("nickchange") || type.equalsIgnoreCase("quit") || type.equalsIgnoreCase("user_channel_mode"))) {
                            calendar.setTimeInMillis(next.getTime());
                            insertEvent(adapter, e, true, calendar.get(Calendar.DAY_OF_YEAR) == lastCollapsedDay);
                        } else {
                            insertEvent(adapter, e, true, false);
                        }
                        if(trace != null)
                            trace.incrementMetric("insertEvent", 1);
                    }
                    adapter.insertLastSeenEIDMarker();
                    if(trace != null)
                        trace.stop();
                    Log.i("IRCCloud", "Backlog rendering took: " + (System.currentTimeMillis() - start) + "ms");
                    //Debug.stopMethodTracing();
                    avgInsertTime = 0;
                    //adapter.notifyDataSetChanged();
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            if(buffer != null) {
                                headerView.setVisibility(View.GONE);
                                if (buffer.getMin_eid() > 0 && earliest_eid > buffer.getMin_eid() && conn.ready && conn.getState() == NetworkConnection.STATE_CONNECTED && !(msgid != null && msgids.containsKey(msgid))) {
                                    if (buffer.getDeferred() > 0) {
                                        backlogFailed.setVisibility(View.GONE);
                                        loadBacklogButton.setVisibility(View.GONE);
                                        headerView.setVisibility(View.VISIBLE);
                                        requestingBacklog = true;
                                        buffer.setDeferred(0);
                                        NetworkConnection.getInstance().request_backlog(buffer.getCid(), buffer.getBid(), earliest_eid);
                                    } else {
                                        loadBacklogButton.setVisibility(View.VISIBLE);
                                    }
                                } else {
                                    loadBacklogButton.setVisibility(View.GONE);
                                }
                            }
                        }
                    });
                }
            }
            if (conn.getReconnectTimestamp() == 0 && conn.getState() == NetworkConnection.STATE_CONNECTED)
                conn.schedule_idle_timer();
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    update_global_msg();
                }
            });
        }

        try {
            if (mainFormatTask != null)
                mainFormatTask.cancel(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
        mainFormatTask = new FormatTask(adapter, events);
        mainFormatTask.execute((Void)null);
    }

    private void update_top_unread(int first) {
        if (adapter != null && buffer != null) {
            try {
                int markerPos = adapter.getLastSeenEIDPosition();
                if (markerPos >= 0 && ((first > (markerPos + 1) && buffer.getUnread() > 0) || adapter.getCount() == 0)) {
                    if (pref_trackUnread) {
                        int highlights = adapter.getUnreadHighlightsAbovePosition(first);
                        int count = (first - markerPos - 1) - highlights;
                        StringBuilder txt = new StringBuilder();
                        if (highlights > 0) {
                            if (highlights == 1)
                                txt.append("mention");
                            else if (highlights > 0)
                                txt.append("mentions");
                            highlightsTopLabel.setText(String.valueOf(highlights));
                            highlightsTopLabel.setVisibility(View.VISIBLE);

                            if (count > 0)
                                txt.append(" and ");
                        } else {
                            highlightsTopLabel.setVisibility(View.GONE);
                        }
                        if (markerPos == 0) {
                            long seconds = (long) Math.ceil((earliest_eid - buffer.getLast_seen_eid()) / 1000000.0);
                            if (seconds <= 0) {
                                if (count < 0) {
                                    hideView(unreadTopView);
                                    return;
                                } else {
                                    if (count == 1)
                                        txt.append(count).append(" unread message");
                                    else if (count > 0)
                                        txt.append(count).append(" unread messages");
                                }
                            } else {
                                int minutes = (int) Math.ceil(seconds / 60.0);
                                int hours = (int) Math.ceil(seconds / 60.0 / 60.0);
                                int days = (int) Math.ceil(seconds / 60.0 / 60.0 / 24.0);
                                if (hours >= 24) {
                                    if (days == 1)
                                        txt.append(days).append(" day of unread messages");
                                    else
                                        txt.append(days).append(" days of unread messages");
                                } else if (hours > 0) {
                                    if (hours == 1)
                                        txt.append(hours).append(" hour of unread messages");
                                    else
                                        txt.append(hours).append(" hours of unread messages");
                                } else if (minutes > 0) {
                                    if (minutes == 1)
                                        txt.append(minutes).append(" minute of unread messages");
                                    else
                                        txt.append(minutes).append(" minutes of unread messages");
                                } else {
                                    if (seconds == 1)
                                        txt.append(seconds).append(" second of unread messages");
                                    else
                                        txt.append(seconds).append(" seconds of unread messages");
                                }
                            }
                        } else {
                            if (count == 1)
                                txt.append(count).append(" unread message");
                            else if (count > 0)
                                txt.append(count).append(" unread messages");
                        }
                        unreadTopLabel.setText(txt);
                        showView(unreadTopView);
                    } else {
                        hideView(unreadTopView);
                    }
                } else {
                    if (markerPos > 0) {
                        hideView(unreadTopView);
                        if (adapter.data.size() > 0 && ready) {
                            if (heartbeatTask != null)
                                heartbeatTask.cancel(true);
                            heartbeatTask = new HeartbeatTask();
                            heartbeatTask.execute((Void) null);
                        }
                    }
                }
            } catch (IllegalStateException e) {
                //The list view wasn't on screen yet
                NetworkConnection.printStackTraceToCrashlytics(e);
            }
        }
    }

    private class UnreadRefreshRunnable implements Runnable {
        @Override
        public void run() {
            update_unread();
        }
    }

    UnreadRefreshRunnable unreadRefreshRunnable = null;

    private void update_unread() {
        if (unreadRefreshRunnable != null) {
            mHandler.removeCallbacks(unreadRefreshRunnable);
            unreadRefreshRunnable = null;
        }

        if (newMsgs > 0) {
            /*int minutes = (int)((System.currentTimeMillis() - newMsgTime)/60000);

			if(minutes < 1)
				unreadBottomLabel.setText("Less than a minute of chatter (");
			else if(minutes == 1)
				unreadBottomLabel.setText("1 minute of chatter (");
			else
				unreadBottomLabel.setText(minutes + " minutes of chatter (");
			if(newMsgs == 1)
				unreadBottomLabel.setText(unreadBottomLabel.getText() + "1 message)");
			else
				unreadBottomLabel.setText(unreadBottomLabel.getText() + (newMsgs + " messages)"));*/

            String txt = "";
            int msgCnt = newMsgs - newHighlights;
            if (newHighlights > 0) {
                if (newHighlights == 1)
                    txt = "mention";
                else
                    txt = "mentions";
                if (msgCnt > 0)
                    txt += " and ";
                highlightsBottomLabel.setText(String.valueOf(newHighlights));
                highlightsBottomLabel.setVisibility(View.VISIBLE);
            } else {
                highlightsBottomLabel.setVisibility(View.GONE);
            }
            if (msgCnt == 1)
                txt += msgCnt + " unread message";
            else if (msgCnt > 0)
                txt += msgCnt + " unread messages";
            unreadBottomLabel.setText(txt);
            showView(unreadBottomView);
            unreadRefreshRunnable = new UnreadRefreshRunnable();
            mHandler.postDelayed(unreadRefreshRunnable, 10000);
        }
    }

    private class StatusRefreshRunnable implements Runnable {
        String status;
        JsonNode fail_info;

        public StatusRefreshRunnable(String status, JsonNode fail_info) {
            this.status = status;
            this.fail_info = fail_info;
        }

        @Override
        public void run() {
            update_status(status, fail_info);
        }
    }

    StatusRefreshRunnable statusRefreshRunnable = null;

    public static String ordinal(int i) {
        String[] sufixes = new String[]{"th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"};
        switch (i % 100) {
            case 11:
            case 12:
            case 13:
                return i + "th";
            default:
                return i + sufixes[i % 10];

        }
    }

    private void update_status(String status, JsonNode fail_info) {
        if (statusRefreshRunnable != null) {
            mHandler.removeCallbacks(statusRefreshRunnable);
            statusRefreshRunnable = null;
        }

        statusView.setTextColor(colorScheme.connectionBarTextColor);
        statusView.setBackgroundColor(colorScheme.connectionBarColor);

        switch (status) {
            case "connected_ready":
                statusView.setVisibility(View.GONE);
                statusView.setText("");
                break;
            case "quitting":
                statusView.setVisibility(View.VISIBLE);
                statusView.setText("Disconnecting");
                break;
            case "disconnected":
                statusView.setVisibility(View.VISIBLE);
                if (fail_info.has("type") && fail_info.get("type").asText().length() > 0) {
                    String text = "Disconnected: ";
                    if (fail_info.get("type").asText().equals("connecting_restricted")) {
                        text = EventsList.reason(fail_info.get("reason").asText());
                        if (text.equals(fail_info.get("reason").asText()))
                            text = "You can’t connect to this server with a free account.";
                    } else if (fail_info.get("type").asText().equals("connection_blocked")) {
                        text = "Disconnected - Connections to this server have been blocked";
                    } else {
                        if (fail_info.has("type") && fail_info.get("type").asText().equals("killed"))
                            text = "Disconnected - Killed: ";
                        else if (fail_info.has("type") && fail_info.get("type").asText().equals("connecting_failed"))
                            text = "Disconnected: Failed to connect - ";
                        if (fail_info.has("reason")) {
                            if(fail_info.get("reason").asText().equals("ssl_verify_error")) {
                                text = "Strict transport security error: " + EventsList.SSLreason(fail_info.get("ssl_verify_error"));
                            } else {
                                text += EventsList.reason(fail_info.get("reason").asText());
                            }
                        }
                    }
                    statusView.setText(text);
                    statusView.setTextColor(colorScheme.networkErrorColor);
                    statusView.setBackgroundColor(colorScheme.networkErrorBackgroundColor);
                } else {
                    statusView.setText("Disconnected. Tap to reconnect.");
                }
                break;
            case "queued":
                statusView.setVisibility(View.VISIBLE);
                statusView.setText("Connection queued");
                break;
            case "connecting":
                statusView.setVisibility(View.VISIBLE);
                statusView.setText("Connecting");
                break;
            case "connected":
                statusView.setVisibility(View.VISIBLE);
                statusView.setText("Connected");
                break;
            case "connected_joining":
                statusView.setVisibility(View.VISIBLE);
                statusView.setText("Connected: Joining Channels");
                break;
            case "pool_unavailable":
                statusView.setVisibility(View.VISIBLE);
                statusView.setText("Connection temporarily unavailable");
                statusView.setTextColor(colorScheme.networkErrorColor);
                statusView.setBackgroundColor(colorScheme.networkErrorBackgroundColor);
                break;
            case "waiting_to_retry":
                try {
                    statusView.setVisibility(View.VISIBLE);
                    long seconds = (fail_info.get("timestamp").asLong() + fail_info.get("retry_timeout").asLong() - conn.clockOffset) - System.currentTimeMillis() / 1000;
                    if (seconds > 0) {
                        String text = "Disconnected";
                        if (fail_info.has("reason") && fail_info.get("reason").asText().length() > 0) {
                            String reason = fail_info.get("reason").asText();
                            reason = EventsList.reason(reason);
                            text += ": " + reason + ". ";
                        } else
                            text += "; ";
                        text += "Reconnecting in ";
                        int minutes = (int) (seconds / 60.0);
                        int hours = (int) (seconds / 60.0 / 60.0);
                        int days = (int) (seconds / 60.0 / 60.0 / 24.0);
                        if (days > 0) {
                            if (days == 1)
                                text += days + " day.";
                            else
                                text += days + " days.";
                        } else if (hours > 0) {
                            if (hours == 1)
                                text += hours + " hour.";
                            else
                                text += hours + " hours.";
                        } else if (minutes > 0) {
                            if (minutes == 1)
                                text += minutes + " minute.";
                            else
                                text += minutes + " minutes.";
                        } else {
                            if (seconds == 1)
                                text += seconds + " second.";
                            else
                                text += seconds + " seconds.";
                        }
                        int attempts = fail_info.get("attempts").asInt();
                        if (attempts > 1)
                            text += " (" + ordinal(attempts) + " attempt)";
                        statusView.setText(text);
                        statusView.setTextColor(colorScheme.networkErrorColor);
                        statusView.setBackgroundColor(colorScheme.networkErrorBackgroundColor);
                        statusRefreshRunnable = new StatusRefreshRunnable(status, fail_info);
                    } else {
                        statusView.setVisibility(View.VISIBLE);
                        statusView.setText("Ready to connect, waiting our turn…");
                    }
                    mHandler.postDelayed(statusRefreshRunnable, 500);
                } catch (Exception e) {
                    NetworkConnection.printStackTraceToCrashlytics(e);
                }
                break;
            case "ip_retry":
                statusView.setVisibility(View.VISIBLE);
                statusView.setText("Trying another IP address");
                break;
        }

    }

    private void update_global_msg() {
        if (globalMsgView != null) {
            if (conn != null && conn.globalMsg != null) {
                globalMsg.setText(Html.fromHtml(conn.globalMsg));
                globalMsgView.setVisibility(View.VISIBLE);
                globalMsgView.findViewById(R.id.dismissGlobalMessage).setVisibility(View.VISIBLE);
            } else {
                if(buffer != null && buffer.getType() != null && buffer.isConversation() && !buffer.isMPDM() && ignore != null && buffer_usermask != null && ignore.match(buffer_usermask)) {
                    globalMsg.setText(Html.fromHtml("Ignoring: <b>" + ignore.getMatch(buffer_usermask) + "</b><br/>This hides channel messages and prevents message notifications.  Archive to hide completely."));
                    globalMsgView.setVisibility(View.VISIBLE);
                    globalMsgView.findViewById(R.id.dismissGlobalMessage).setVisibility(View.GONE);
                } else {
                    globalMsgView.setVisibility(View.GONE);
                }
            }
        }
    }

    @Override
    public void onPause() {
        super.onPause();

        if(getActivity() == null || !((BaseActivity)getActivity()).isMultiWindow()) {
            if (statusRefreshRunnable != null) {
                mHandler.removeCallbacks(statusRefreshRunnable);
                statusRefreshRunnable = null;
            }
            try {
                getListView().setOnScrollListener(null);
            } catch (Exception e) {
            }
            ready = false;
        }
    }

    public void uncacheFileId(String fileID) {
        filePropsCache.remove(fileID);
        new RefreshTask().execute((Void)null);
    }

    public void hideFileId(String fileID) {
        hiddenFileIDs.add(fileID);
        new RefreshTask().execute((Void)null);
    }

    public void onIRCEvent(int what, final Object obj) {
        IRCCloudJSONObject e;

        switch (what) {
            case NetworkConnection.EVENT_OOB_FAILED:
            case NetworkConnection.EVENT_BACKLOG_FAILED:
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        headerView.setVisibility(View.GONE);
                        backlogFailed.setVisibility(View.VISIBLE);
                        loadBacklogButton.setVisibility(View.VISIBLE);
                    }
                });
                break;
            case NetworkConnection.EVENT_OOB_END:
                OOBFetcher fetcher = (OOBFetcher)obj;
                if(buffer != null && (fetcher.getBid() == -1 || fetcher.getBid() == buffer.getBid())) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            if (refreshTask != null)
                                refreshTask.cancel(true);
                            refreshTask = new RefreshTask();
                            refreshTask.execute((Void) null);
                        }
                    });
                }
                break;
            case NetworkConnection.EVENT_BACKLOG_END:
            case NetworkConnection.EVENT_CONNECTIVITY:
            case NetworkConnection.EVENT_USERINFO:
                if(buffer != null) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            if (refreshTask != null)
                                refreshTask.cancel(true);
                            refreshTask = new RefreshTask();
                            refreshTask.execute((Void) null);
                        }
                    });
                }
                break;
            case NetworkConnection.EVENT_CONNECTIONLAG:
                try {
                    IRCCloudJSONObject object = (IRCCloudJSONObject) obj;
                    if (server != null && buffer != null && object.cid() == buffer.getCid()) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                update_status(server.getStatus(), server.getFail_info());
                            }
                        });
                    }
                } catch (Exception ex) {
                    NetworkConnection.printStackTraceToCrashlytics(ex);
                }
                break;
            case NetworkConnection.EVENT_STATUSCHANGED:
                try {
                    final IRCCloudJSONObject object = (IRCCloudJSONObject) obj;
                    if (buffer != null && object.cid() == buffer.getCid()) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                update_status(object.getString("new_status"), object.getJsonObject("fail_info"));
                            }
                        });
                    }
                } catch (Exception ex) {
                    NetworkConnection.printStackTraceToCrashlytics(ex);
                }
                break;
            case NetworkConnection.EVENT_SETIGNORES:
                e = (IRCCloudJSONObject) obj;
                if (buffer != null && e.cid() == buffer.getCid()) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            if (refreshTask != null)
                                refreshTask.cancel(true);
                            refreshTask = new RefreshTask();
                            refreshTask.execute((Void) null);
                        }
                    });
                }
                break;
            case NetworkConnection.EVENT_MESSAGECHANGE:
                e = (IRCCloudJSONObject) obj;
                if (buffer != null && e.bid() == buffer.getBid()) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            if (refreshTask != null)
                                refreshTask.cancel(true);
                            refreshTask = new RefreshTask();
                            refreshTask.execute((Void) null);
                        }
                    });
                }
                break;
            case NetworkConnection.EVENT_HEARTBEATECHO:
                try {
                    if (buffer != null && adapter != null && adapter.data.size() > 0) {
                        if (buffer.getLast_seen_eid() == adapter.data.get(adapter.data.size() - 1).eid || !pref_trackUnread) {
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    hideView(unreadTopView);
                                }
                            });
                        }
                    }
                } catch (Exception ex) {
                }
                break;
            case NetworkConnection.EVENT_CHANNELTOPIC:
            case NetworkConnection.EVENT_JOIN:
            case NetworkConnection.EVENT_PART:
            case NetworkConnection.EVENT_NICKCHANGE:
            case NetworkConnection.EVENT_QUIT:
            case NetworkConnection.EVENT_KICK:
            case NetworkConnection.EVENT_CHANNELMODE:
            case NetworkConnection.EVENT_USERMODE:
            case NetworkConnection.EVENT_USERCHANNELMODE:
                e = (IRCCloudJSONObject) obj;
                if (buffer != null && e.bid() == buffer.getBid()) {
                    final Event event = EventsList.getInstance().getEvent(e.eid(), e.bid());
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            if (adapter != null)
                                insertEvent(adapter, event, false, false);
                        }
                    });
                }
                break;
            case NetworkConnection.EVENT_SELFDETAILS:
            case NetworkConnection.EVENT_BUFFERMSG:
                final Event event = (Event) obj;
                if (buffer != null && event.bid == buffer.getBid()) {
                    if (event.from != null && event.from.equalsIgnoreCase(buffer.getName()) && event.reqid == -1) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                if (adapter != null)
                                    adapter.clearPending();
                            }
                        });
                    } else if (event.reqid != -1) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                if (adapter != null && adapter.data != null) {
                                    for (int i = 0; i < adapter.data.size(); i++) {
                                        Event e = adapter.data.get(i);
                                        if (e.reqid == event.reqid && e.pending) {
                                            if (i > 0) {
                                                Event p = adapter.data.get(i - 1);
                                                if (p.row_type == ROW_TIMESTAMP) {
                                                    adapter.data.remove(p);
                                                    i--;
                                                }
                                            }
                                            adapter.data.remove(e);
                                            i--;
                                        }
                                    }
                                }
                            }
                        });
                    }
                    if (buffer.getScrolledUp() || EventsList.getInstance().getSizeOfBuffer(buffer.getBid()) <= 200) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                insertEvent(adapter, event, false, false);
                                if (event.pending && event.self && adapter != null && getListView() != null) {
                                    getListView().setSelection(adapter.getCount() - 1);
                                }
                            }
                        });
                    }
                }
                Buffer b = BuffersList.getInstance().getBuffer(event.bid);
                if (b != null && !b.getScrolledUp() && EventsList.getInstance().getSizeOfBuffer(b.getBid()) > 200) {
                    EventsList.getInstance().pruneEvents(b.getBid());
                    if (buffer != null && b.getBid() == buffer.getBid()) {
                        if (b.getLast_seen_eid() < event.eid && unreadTopView.getVisibility() == View.GONE)
                            b.setLast_seen_eid(event.eid);

                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                if (refreshTask != null)
                                    refreshTask.cancel(true);
                                refreshTask = new RefreshTask();
                                refreshTask.execute((Void) null);
                            }
                        });
                    }
                }
                break;
            case NetworkConnection.EVENT_AWAY:
                e = (IRCCloudJSONObject) obj;
                if(buffer == null || e.cid() != buffer.getCid())
                    break;
            case NetworkConnection.EVENT_SELFBACK:
                if (server != null) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            if (server.getAway() != null && server.getAway().length() > 0) {
                                awayTxt.setText(ColorFormatter.strip("Away (" + server.getAway() + ")"));
                                awayView.setVisibility(View.VISIBLE);
                            } else {
                                awayView.setVisibility(View.GONE);
                            }
                        }
                    });
                }
                break;
            case NetworkConnection.EVENT_GLOBALMSG:
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        update_global_msg();
                    }
                });
                break;
            case NetworkConnection.EVENT_FONT_DOWNLOADED:
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (buffer != null) {
                            if (refreshTask != null)
                                refreshTask.cancel(true);
                            refreshTask = new RefreshTask();
                            refreshTask.execute((Void) null);
                        }
                        if (server != null && server.getAway() != null && server.getAway().length() > 0)
                            awayTxt.setText(ColorFormatter.strip("Away (" + server.getAway() + ")"));
                    }
                });
                break;
            default:
                break;
        }
    }

    public static Resources getSafeResources() {
        return IRCCloudApplication.getInstance().getApplicationContext().getResources();
    }

    public interface MessageViewListener extends OnBufferSelectedListener {
        void onMessageViewReady();

        boolean onMessageLongClicked(Event event);

        void onMessageDoubleClicked(Event event);

        void onFailedMessageClicked(Event event);

        void promptToJoin(String channel, String key, Server server);
    }
} public class NamesListFragment extends DialogFragment {
    JsonNode users;
    UsersAdapter adapter;
    NetworkConnection conn;
    RecyclerView recyclerView;
    TextView empty;
    IRCCloudJSONObject event;

    private class ViewHolder extends RecyclerView.ViewHolder {
        public RowWhoBinding binding;

        public ViewHolder(View v) {
            super(v);
            binding = DataBindingUtil.bind(v);
        }
    }

    private class UsersAdapter extends RecyclerView.Adapter<ViewHolder> {

        @Override
        public int getItemCount() {
            return users.size();
        }

        @Override
        public long getItemId(int pos) {
            return pos;
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = RowWhoBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false).getRoot();
            return new ViewHolder(v);
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, final int position) {
            RowWhoBinding row = holder.binding;
            row.setNick(users.get(position).get("nick").asText());
            if (users.get(position).get("mode").asText().length() > 0)
                row.setName(new SpannableString(" (+" + users.get(position).get("mode").asText() + ")"));
            else
                row.setName(new SpannableString(""));
            row.setServer("");
            row.setMask(ColorFormatter.html_to_spanned(ColorFormatter.irc_to_html(users.get(position).get("usermask").asText())));
            row.executePendingBindings();
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Context ctx = getActivity();
        if(ctx == null)
            return null;

        LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(R.layout.recyclerview, null);
        recyclerView = v.findViewById(R.id.recycler);
        recyclerView.setLayoutManager(new LinearLayoutManager(v.getContext()));
        empty = v.findViewById(android.R.id.empty);
        empty.setText("No results found.");
        if (savedInstanceState != null && savedInstanceState.containsKey("event")) {
            event = new IRCCloudJSONObject(savedInstanceState.getString("event"));
            users = event.getJsonNode("members");
            adapter = new UsersAdapter();
            recyclerView.setAdapter(adapter);
            if(adapter.getItemCount() > 0) {
                empty.setVisibility(View.GONE);
                recyclerView.setVisibility(View.VISIBLE);
            } else {
                empty.setVisibility(View.VISIBLE);
                recyclerView.setVisibility(View.GONE);
            }
        }
        Dialog d = new AlertDialog.Builder(ctx)
                .setTitle("NAMES response for " + event.getString("chan"))
                .setView(v)
                .setNegativeButton("Close", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                })
                .create();
        return d;
    }

    @Override
    public void onSaveInstanceState(Bundle state) {
        state.putString("event", event.toString());
    }

    @Override
    public void setArguments(Bundle args) {
        if(args != null) {
            event = new IRCCloudJSONObject(args.getString("event"));
            users = event.getJsonNode("members");
            if (getActivity() != null && recyclerView != null) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (adapter == null) {
                            adapter = new UsersAdapter();
                            recyclerView.setAdapter(adapter);
                        } else {
                            adapter.notifyDataSetChanged();
                        }
                        if (adapter.getItemCount() > 0) {
                            empty.setVisibility(View.GONE);
                            recyclerView.setVisibility(View.VISIBLE);
                        } else {
                            empty.setVisibility(View.VISIBLE);
                            recyclerView.setVisibility(View.GONE);
                        }
                    }
                });
            }
        }
    }

    public void onResume() {
        super.onResume();
        conn = NetworkConnection.getInstance();

        if (users != null) {
            adapter = new UsersAdapter();
            recyclerView.setAdapter(adapter);
            if(adapter.getItemCount() > 0) {
                empty.setVisibility(View.GONE);
                recyclerView.setVisibility(View.VISIBLE);
            } else {
                empty.setVisibility(View.VISIBLE);
                recyclerView.setVisibility(View.GONE);
            }
        }
    }

    @Override
    public void onPause() {
        super.onPause();
    }
} public class NickservFragment extends DialogFragment {
    Server server;
    EditText pass;
    TextView nick;

    class SaveClickListener implements DialogInterface.OnClickListener {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            if (pass.getText() != null && pass.getText().length() > 0) {
                NetworkConnection.getInstance().set_nspass(server.getCid(), pass.getText().toString(), null);
                dismiss();
            }
        }
    }

    class InstructionsClickListener implements DialogInterface.OnClickListener {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            NetworkConnection.getInstance().ns_help_register(server.getCid(), null);
            dismiss();
        }
    }

    public void setCid(int cid) {
        server = ServersList.getInstance().getServer(cid);
        if (nick != null && server != null) {
            nick.setText("Password for " + server.getNick());
            if (server.getNickserv_pass() != null)
                pass.setText(server.getNickserv_pass());
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        if (server == null)
            return null;

        Context ctx = getActivity();
        if(ctx == null)
            return null;
        LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        View v = inflater.inflate(R.layout.dialog_nickserv, null);
        nick = v.findViewById(R.id.nickname);
        pass = v.findViewById(R.id.password);
        nick.setText("Password for " + server.getNick());
        if (server.getNickserv_pass() != null)
            pass.setText(server.getNickserv_pass());

        String title = "Identify your nickname on ";
        if (server.getName() != null && server.getName().length() > 0)
            title += server.getName();
        else
            title += server.getHostname();

        AlertDialog d = new AlertDialog.Builder(ctx)
                .setTitle(title)
                .setView(v)
                .setPositiveButton("Save", new SaveClickListener())
                .setNeutralButton("Instructions", new InstructionsClickListener())
                .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                })
                .create();
        d.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
        return d;
    }
} public class PinReorderFragment extends DialogFragment implements NetworkConnection.IRCEventHandler {
    private ColorScheme colorScheme = ColorScheme.getInstance();
    private NetworkConnection conn;
    private PinListAdapter adapter;
    private DragSortListView listView = null;
    private DragSortListView.DropListener dropListener = new DragSortListView.DropListener() {
        @Override
        public void drop(int from, int to) {
            Integer i = adapter.data.get(from);
            adapter.data.remove(i);
            if (to >= adapter.data.size())
                adapter.data.add(i);
            else
                adapter.data.add(to, i);
            adapter.notifyDataSetChanged();

            try {
                if (NetworkConnection.getInstance().getUserInfo() != null) {
                    JSONObject prefs = NetworkConnection.getInstance().getUserInfo().prefs;
                    if (prefs == null) {
                        prefs = new JSONObject();
                    }
                    prefs.put("pinnedBuffers", new JSONArray(adapter.data));
                    conn.set_prefs(prefs.toString(), null);
                } else {
                    throw new IllegalStateException();
                }
            } catch (Exception e) {
                e.printStackTrace();
                Toast.makeText(getActivity(), "An error occurred while saving preferences.  Please try again shortly", Toast.LENGTH_SHORT).show();
            }
        }
    };

    private class PinListAdapter extends BaseAdapter {
        private ArrayList<Integer> data;
        private HashMap<String,Integer> nameCounts;
        private DialogFragment ctx;
        int width = 0;

        private class ViewHolder {
            View background;
            TextView label;
            TextView icon;
            TextView drag_handle;
        }

        public PinListAdapter(DialogFragment context) {
            ctx = context;
            data = new ArrayList<>();
            WindowManager wm = (WindowManager) IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
            width = wm.getDefaultDisplay().getWidth();
        }

        public void setItems(ArrayList<Integer> items) {
            data = items;
            nameCounts = new HashMap<>();

            for (Integer bid : data) {
                Buffer b = BuffersList.getInstance().getBuffer(bid);
                if(b != null) {
                    int count = 1;
                    if (nameCounts.containsKey(b.getName()))
                        count += nameCounts.get(b.getName());
                    nameCounts.put(b.getName(), count);
                }
            }
        }

        @Override
        public int getCount() {
            return data.size();
        }

        @Override
        public Object getItem(int position) {
            return data.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            Buffer b = BuffersList.getInstance().getBuffer(data.get(position));
            View row = convertView;
            ViewHolder holder;

            if (row == null) {
                LayoutInflater inflater = ctx.getLayoutInflater(null);
                row = inflater.inflate(R.layout.row_reorder, null);

                holder = new ViewHolder();
                holder.background = row.findViewById(R.id.serverBackground);
                holder.background.setFocusable(false);
                holder.background.setEnabled(false);
                holder.label = row.findViewById(R.id.label);
                holder.icon = row.findViewById(R.id.icon);
                holder.icon.setTypeface(FontAwesome.getTypeface());
                holder.drag_handle = row.findViewById(R.id.drag_handle);
                holder.drag_handle.setTypeface(FontAwesome.getTypeface());
                row.setTag(holder);
            } else {
                holder = (ViewHolder) row.getTag();
            }

            b.showServerSuffix(nameCounts.containsKey(b.getName()) && nameCounts.get(b.getName()) > 1);
            String name = b.getDisplayName() + b.getServerSuffix();
            holder.label.setText(name);
            holder.label.setTextColor(b.getTextColor());
            holder.icon.setText(b.getIcon());
            holder.icon.setTextColor(b.getTextColor());

            holder.drag_handle.setText(FontAwesome.ARROWS);

            if (getShowsDialog())
                row.setBackgroundColor(colorScheme.dialogBackgroundColor);
            else
                row.setBackgroundColor(colorScheme.contentBackgroundColor);

            holder.label.setMinimumWidth(width);
            return row;
        }
    }

    private void refresh() {
        JSONObject prefs = NetworkConnection.getInstance().getUserInfo().prefs;
        if (prefs == null) {
            prefs = new JSONObject();
        }
        JSONArray pinnedBuffers = null;
        try {
            if (prefs.has("pinnedBuffers")) {
                pinnedBuffers = prefs.getJSONArray("pinnedBuffers");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (pinnedBuffers == null)
            pinnedBuffers = new JSONArray();
        ArrayList<Integer> pins = new ArrayList<>();

        try {
            for (int i = 0; i < pinnedBuffers.length(); i++) {
                if(BuffersList.getInstance().getBuffer(pinnedBuffers.getInt(i)) != null)
                    pins.add(pinnedBuffers.getInt(i));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (pins.size() == 0) {
            if (adapter != null) {
                adapter.data.clear();
                adapter.notifyDataSetInvalidated();
            }
            return;
        }

        if (adapter == null) {
            adapter = new PinListAdapter(PinReorderFragment.this);
        }

        adapter.setItems(pins);

        if (listView.getAdapter() == null)
            listView.setAdapter(adapter);
        else
            adapter.notifyDataSetChanged();
    }

    private void init(View v) {
        listView = v.findViewById(android.R.id.list);
        DragSortController controller = new DragSortController(listView);
        controller.setDragHandleId(R.id.drag_handle);
        controller.setSortEnabled(true);
        controller.setDragInitMode(DragSortController.ON_DRAG);

        listView.setOnTouchListener(controller);
        listView.setFloatViewManager(controller);
        listView.setDropListener(dropListener);
        TextView tv = v.findViewById(R.id.hint);
        tv.setTypeface(FontAwesome.getTypeface());
        tv.setText(FontAwesome.ARROWS);
        tv = v.findViewById(R.id.hintText);
        tv.setText(" to reorder the pinned channels");
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        if (getShowsDialog()) {
            return super.onCreateView(inflater, container, savedInstanceState);
        } else {
            final View v = inflater.inflate(R.layout.reorderservers, null);
            init(v);
            listView.setCacheColorHint(0xFFD9E7FF);
            return v;
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Context ctx = getActivity();
        if(ctx == null)
            return null;

        LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(R.layout.reorderservers, null);
        init(v);
        listView.setCacheColorHint(0xfff3f3f3);

        Dialog d = new AlertDialog.Builder(ctx)
                .setTitle("Pinned Channels")
                .setView(v)
                .setNegativeButton("Done", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        NetworkConnection.getInstance().removeHandler(PinReorderFragment.this);
                    }
                })
                .create();
        return d;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        conn = NetworkConnection.getInstance();
        conn.addHandler(this);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if(conn != null)
            conn.removeHandler(this);
    }

    public void onResume() {
        super.onResume();
        refresh();
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
    }

    public void onIRCEvent(int what, Object obj) {
        switch (what) {
            case NetworkConnection.EVENT_USERINFO:
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        refresh();
                    }
                });
                break;
            default:
                break;
        }
    }
} public class ServerReorderFragment extends DialogFragment implements NetworkConnection.IRCEventHandler {
    private ColorScheme colorScheme = ColorScheme.getInstance();
    private NetworkConnection conn;
    private ServerListAdapter adapter;
    private RefreshTask refreshTask = null;
    private DragSortListView listView = null;
    private DragSortListView.DropListener dropListener = new DragSortListView.DropListener() {
        @Override
        public void drop(int from, int to) {
            Server s = adapter.data.get(from);
            adapter.data.remove(from);
            if (to >= adapter.data.size())
                adapter.data.add(s);
            else
                adapter.data.add(to, s);
            adapter.notifyDataSetChanged();

            String cids = "";
            for (int i = 0; i < adapter.data.size(); i++) {
                s = adapter.data.get(i);
                s.setOrder(i + 1);
                if (cids.length() > 0)
                    cids += ",";
                cids += s.getCid();
            }
            NetworkConnection.getInstance().reorder_connections(cids, null);
        }
    };

    private class ServerListAdapter extends BaseAdapter {
        ArrayList<Server> data;
        private DialogFragment ctx;
        int width = 0;

        private class ViewHolder {
            View background;
            TextView label;
            TextView icon;
            TextView drag_handle;
        }

        public ServerListAdapter(DialogFragment context) {
            ctx = context;
            data = new ArrayList<Server>();
            WindowManager wm = (WindowManager) IRCCloudApplication.getInstance().getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
            width = wm.getDefaultDisplay().getWidth();
        }

        public void setItems(ArrayList<Server> items) {
            data = items;
        }

        @Override
        public int getCount() {
            return data.size();
        }

        @Override
        public Object getItem(int position) {
            return data.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            Server s = data.get(position);
            View row = convertView;
            ViewHolder holder;

            if (row == null) {
                LayoutInflater inflater = ctx.getLayoutInflater(null);
                row = inflater.inflate(R.layout.row_reorder, null);

                holder = new ViewHolder();
                holder.background = row.findViewById(R.id.serverBackground);
                holder.background.setFocusable(false);
                holder.background.setEnabled(false);
                holder.label = row.findViewById(R.id.label);
                holder.icon = row.findViewById(R.id.icon);
                holder.icon.setTypeface(FontAwesome.getTypeface());
                holder.drag_handle = row.findViewById(R.id.drag_handle);
                holder.drag_handle.setTypeface(FontAwesome.getTypeface());
                row.setTag(holder);
            } else {
                holder = (ViewHolder) row.getTag();
            }

            if (s.getName() != null && s.getName().length() > 0)
                holder.label.setText(s.getName());
            else
                holder.label.setText(s.getHostname());

            if (s.getSsl() > 0)
                holder.icon.setText(FontAwesome.SHIELD);
            else
                holder.icon.setText(FontAwesome.GLOBE);

            if (s.getStatus() != null && s.getStatus().equals("connected_ready")) {
                holder.icon.setTextColor(colorScheme.bufferTextColor);
                holder.label.setTextColor(colorScheme.bufferTextColor);
            } else {
                holder.icon.setTextColor(colorScheme.inactiveBufferTextColor);
                holder.label.setTextColor(colorScheme.inactiveBufferTextColor);
            }

            holder.drag_handle.setText(FontAwesome.ARROWS);

            if (getShowsDialog())
                row.setBackgroundColor(colorScheme.dialogBackgroundColor);
            else
                row.setBackgroundColor(colorScheme.contentBackgroundColor);

            holder.label.setMinimumWidth(width);
            return row;
        }
    }

    private void refresh(ArrayList<Server> servers) {
        if (servers == null) {
            if (adapter != null) {
                adapter.data.clear();
                adapter.notifyDataSetInvalidated();
            }
            return;
        }

        if (adapter == null) {
            adapter = new ServerListAdapter(ServerReorderFragment.this);
        }

        adapter.setItems(servers);

        if (listView.getAdapter() == null)
            listView.setAdapter(adapter);
        else
            adapter.notifyDataSetChanged();
    }

    private class RefreshTask extends AsyncTaskEx<Void, Void, Void> {
        ArrayList<Server> servers = new ArrayList<Server>();

        @Override
        protected Void doInBackground(Void... params) {
            SparseArray<Server> s = ServersList.getInstance().getServers();
            for (int i = 0; i < s.size(); i++) {
                servers.add(s.valueAt(i));
            }
            Collections.sort(servers);
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            if (!isCancelled()) {
                refresh(servers);
                refreshTask = null;
            }
        }
    }

    private void init(View v) {
        listView = v.findViewById(android.R.id.list);
        DragSortController controller = new DragSortController(listView);
        controller.setDragHandleId(R.id.drag_handle);
        controller.setSortEnabled(true);
        controller.setDragInitMode(DragSortController.ON_DRAG);

        listView.setOnTouchListener(controller);
        listView.setFloatViewManager(controller);
        listView.setDropListener(dropListener);
        TextView tv = v.findViewById(R.id.hint);
        tv.setTypeface(FontAwesome.getTypeface());
        tv.setText(FontAwesome.ARROWS);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        if (getShowsDialog()) {
            return super.onCreateView(inflater, container, savedInstanceState);
        } else {
            final View v = inflater.inflate(R.layout.reorderservers, null);
            init(v);
            listView.setCacheColorHint(0xFFD9E7FF);
            return v;
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Context ctx = getActivity();
        if(ctx == null)
            return null;

        LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(R.layout.reorderservers, null);
        init(v);
        listView.setCacheColorHint(0xfff3f3f3);

        Dialog d = new AlertDialog.Builder(ctx)
                .setTitle("Connections")
                .setView(v)
                .setNegativeButton("Done", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        NetworkConnection.getInstance().removeHandler(ServerReorderFragment.this);
                    }
                })
                .create();
        return d;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        conn = NetworkConnection.getInstance();
        conn.addHandler(this);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if(conn != null)
            conn.removeHandler(this);
    }

    public void onResume() {
        super.onResume();
        ArrayList<Server> servers = new ArrayList<Server>();
        SparseArray<Server> s = ServersList.getInstance().getServers();
        for (int i = 0; i < s.size(); i++) {
            servers.add(s.valueAt(i));
        }
        Collections.sort(servers);
        refresh(servers);
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
    }

    public void onIRCEvent(int what, Object obj) {
        switch (what) {
            case NetworkConnection.EVENT_MAKESERVER:
            case NetworkConnection.EVENT_REORDERCONNECTIONS:
            case NetworkConnection.EVENT_CONNECTIONDELETED:
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (refreshTask != null)
                            refreshTask.cancel(true);
                        refreshTask = new RefreshTask();
                        refreshTask.execute((Void) null);
                    }
                });
                break;
            default:
                break;
        }
    }
} public class SpamFragment extends DialogFragment {
    Server server;
    ArrayList<Buffer> buffers;
    ArrayList<Buffer> buffersToRemove;
    ListView listView;

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        final Context ctx = getActivity();
        if (ctx == null)
            return null;

        if(server == null && savedInstanceState != null && savedInstanceState.containsKey("cid"))
            server = ServersList.getInstance().getServer(savedInstanceState.getInt("cid"));

        if(server == null)
            throw new IllegalArgumentException("invalid CID");

        if(buffers == null && savedInstanceState != null && savedInstanceState.containsKey("buffers")) {
            buffers = (ArrayList<Buffer>) savedInstanceState.getSerializable("buffers");
        } else {
            buffers = new ArrayList<>();
            for(Buffer b : BuffersList.getInstance().getBuffersForServer(server.getCid())) {
                if(b.getArchived() == 0 && b.getType().equals("conversation"))
                    buffers.add(b);
            }
        }

        ArrayList<String> itemsList = new ArrayList<>();
        for(Buffer b : buffers) {
            itemsList.add(b.getName());
        }

        if(buffersToRemove == null && savedInstanceState != null && savedInstanceState.containsKey("buffersToRemove")) {
            buffersToRemove = (ArrayList<Buffer>) savedInstanceState.getSerializable("buffersToRemove");
        } else {
            buffersToRemove = new ArrayList<>(buffers);
        }

        CharSequence[] items = itemsList.toArray(new CharSequence[itemsList.size()]);
        LayoutInflater inflater = LayoutInflater.from(ctx);
        View v = inflater.inflate(R.layout.dialog_spam, null);
        listView = v.findViewById(android.R.id.list);
        listView.setAdapter(new ArrayAdapter<>(ctx, R.layout.row_spam, items));

        for(int i = 0; i < buffers.size(); i++) {
            listView.setItemChecked(i, buffersToRemove.contains(buffers.get(i)));
        }

        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
                SparseBooleanArray checked = listView.getCheckedItemPositions();
                buffersToRemove.clear();
                for(int i = 0; i < buffers.size(); i++) {
                    if(checked.get(i, false))
                        buffersToRemove.add(buffers.get(i));
                }
            }
        });

        return new AlertDialog.Builder(ctx)
                .setView(v)
                .setPositiveButton("Delete", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        for(Buffer b : buffersToRemove) {
                            NetworkConnection.getInstance().deleteBuffer(b.getCid(), b.getBid(), null);
                        }

                        new AlertDialog.Builder(ctx)
                                .setTitle(server.getName() + " (" + server.getHostname() + ":" + (server.getPort()) + ")")
                                .setMessage(buffersToRemove.size() + " conversations were deleted")
                                .setNegativeButton("Close", null)
                                .show();
                    }
                })
                .setNegativeButton("Cancel", null)
                .create();
    }

    public void setCid(int cid) {
        server = ServersList.getInstance().getServer(cid);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        if(server != null)
            outState.putInt("cid", server.getCid());
        if(buffers != null)
            outState.putSerializable("buffers", buffers);
        if(buffersToRemove != null)
            outState.putSerializable("buffersToRemove", buffersToRemove);
    }
} public class TextListFragment extends DialogFragment {
    private TextView textView;
    private String title = null;
    private String text = null;
    public boolean dismissed = false;
    public String type;

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Context ctx = getActivity();
        if(ctx == null)
            return null;

        LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(R.layout.dialog_textlist, null);
        textView = v.findViewById(R.id.textView);
        textView.setHorizontallyScrolling(true);
        textView.setMovementMethod(new ScrollingMovementMethod());

        if (savedInstanceState != null && savedInstanceState.containsKey("text")) {
            text = savedInstanceState.getString("text");
        }

        if(text != null) {
            setText(text);
        }

        Dialog d = new AlertDialog.Builder(ctx)
                .setView(v)
                .setTitle(title)
                .setNegativeButton("Close", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                })
                .create();
        return d;
    }

    @Override
    public void onDismiss(DialogInterface dialog) {
        super.onDismiss(dialog);
        dismissed = true;
        if(getActivity() != null && ((MainActivity)getActivity()).help_fragment == this)
            ((MainActivity)getActivity()).help_fragment = null;
    }

    @Override
    public void onCancel(DialogInterface dialog) {
        super.onCancel(dialog);
        dismissed = true;
        if(getActivity() != null && ((MainActivity)getActivity()).help_fragment == this)
            ((MainActivity)getActivity()).help_fragment = null;
    }

    @Override
    public void onSaveInstanceState(Bundle state) {
        state.putString("text", text);
    }

    public void refresh() {
        Bundle args = getArguments();

        if(args.containsKey("title")) {
            title = args.getString("title");
            if(getDialog() != null)
                getDialog().setTitle(title);
        }

        if(args.containsKey("text")) {
            text = args.getString("text");
            if(textView != null)
                setText(text);
        }
    }

    private void setText(String text) {
        SpannableStringBuilder sb = new SpannableStringBuilder(text);
        for (int i = 0; i < 100; i++)
            sb.setSpan(new TabStopSpan.Standard(i * 300), 0, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        textView.setText(sb, TextView.BufferType.SPANNABLE);
    }

    @Override
    public void setArguments(Bundle args) {
        super.setArguments(args);
        refresh();
    }

    @Override
    public void onPause() {
        super.onPause();
    }
} public class UsersListFragment extends Fragment implements NetworkConnection.IRCEventHandler {
    private static final int TYPE_HEADING = 0;
    private static final int TYPE_USER = 1;

    private NetworkConnection conn;
    private UserListAdapter adapter;
    private OnUserSelectedListener mListener;
    private int cid = -1;
    private int bid = -1;
    private String channel;
    private static Timer tapTimer = null;
    private TimerTask tapTimerTask = null;
    private RecyclerView recyclerView = null;

    private ColorScheme colorScheme = ColorScheme.getInstance();

    public RecyclerView getRecyclerView() {
        return recyclerView;
    }

    public static class UserListEntry {
        public int type;
        public String text;
        public String count;
        public int bg_color;
        public int heading_color;
        public boolean last;
        public User user;
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        public RowUserBinding binding;

        public ViewHolder(View v) {
            super(v);
            binding = DataBindingUtil.bind(v);
        }
    }

    private class UserListAdapter extends RecyclerView.Adapter<ViewHolder> {
        ArrayList<UserListEntry> data;

        public UserListAdapter() {
            data = new ArrayList<>(50);
        }

        public void setItems(ArrayList<UserListEntry> items) {
            data = items;
        }

        public UserListEntry buildItem(int type, String text, String count, int bg_color, int heading_color, User user, boolean last) {
            UserListEntry e = new UserListEntry();
            e.type = type;
            e.text = text;
            e.count = count;
            e.bg_color = bg_color;
            e.heading_color = heading_color;
            e.user = user;
            e.last = last;
            return e;
        }

        @Override
        public int getItemCount() {
            return data.size();
        }

        public Object getItem(int position) {
            return data.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = RowUserBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false).getRoot();
            return new ViewHolder(v);
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, final int position) {
            UserListEntry e = data.get(position);
            RowUserBinding row = holder.binding;

            row.setUser(e);
            row.setAway(e.user != null && e.user.away > 0);
            row.getRoot().setOnLongClickListener(new OnItemLongClickListener(position));
            row.getRoot().setOnClickListener(new OnItemClickListener(position));

            try {
                row.setMono(PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("monospace", false));
            } catch (Exception e1) {
                row.setMono(false);
            }

            if (e.type == TYPE_HEADING) {
                row.getRoot().setFocusable(false);
                row.getRoot().setEnabled(false);
            } else {
                row.getRoot().setFocusable(true);
                row.getRoot().setEnabled(true);
            }
            row.executePendingBindings();
        }
    }

    private void addUsersFromList(ArrayList<UserListEntry> entries, ArrayList<User> users, String heading, String symbol, int bg_color, int heading_color) {
        if (users.size() > 0 && symbol != null) {
            entries.add(adapter.buildItem(TYPE_HEADING, heading, users.size() > 0 ? symbol + String.valueOf(users.size()) : null, bg_color, heading_color, null, false));
            for (int i = 0; i < users.size(); i++) {
                User user = users.get(i);
                entries.add(adapter.buildItem(TYPE_USER, user.getDisplayName(), null, bg_color, heading_color, user, bg_color != colorScheme.row_members_bg_drawable && i == users.size() - 1));
            }
        }
    }

    private void refresh(ArrayList<User> users) {
        if (users == null) {
            if (adapter != null) {
                adapter.data.clear();
                adapter.notifyDataSetChanged();
            }
            return;
        }

        ArrayList<UserListEntry> entries = new ArrayList<>();
        ArrayList<User> opers = new ArrayList<>();
        ArrayList<User> owners = new ArrayList<>();
        ArrayList<User> admins = new ArrayList<>();
        ArrayList<User> ops = new ArrayList<>();
        ArrayList<User> halfops = new ArrayList<>();
        ArrayList<User> voiced = new ArrayList<>();
        ArrayList<User> members = new ArrayList<>();
        boolean showSymbol = false;
        try {
            if (conn != null && conn.getUserInfo() != null && conn.getUserInfo().prefs != null)
                showSymbol = conn.getUserInfo().prefs.getBoolean("mode-showsymbol");
        } catch (JSONException e) {
        }

        ObjectNode PREFIX = null;
        Server s = ServersList.getInstance().getServer(cid);
        if (s != null)
            PREFIX = s.PREFIX;

        if (PREFIX == null) {
            PREFIX = new ObjectMapper().createObjectNode();
            PREFIX.put(s != null ? s.MODE_OPER : "Y", "!");
            PREFIX.put(s != null ? s.MODE_OWNER : "q", "~");
            PREFIX.put(s != null ? s.MODE_ADMIN : "a", "&");
            PREFIX.put(s != null ? s.MODE_OP : "o", "@");
            PREFIX.put(s != null ? s.MODE_HALFOP : "h", "%");
            PREFIX.put(s != null ? s.MODE_VOICED : "v", "+");
        }

        if (adapter == null) {
            adapter = new UserListAdapter();
        }

        String operGroupMode = "Y";
        for (int i = 0; i < users.size(); i++) {
            User user = users.get(i);
            if(s != null && user.nick.equals(s.getNick()))
                s.setFrom(user.getDisplayName());
            String mode = user.mode.toLowerCase();
            if (mode.contains(s != null ? s.MODE_OPER.toLowerCase() : "y") && (PREFIX.has(s != null ? s.MODE_OPER : "Y") || PREFIX.has(s != null ? s.MODE_OPER.toLowerCase() : "y"))) {
                opers.add(user);
                if(s != null) {
                    if (user.mode.contains(s.MODE_OPER))
                        operGroupMode = s.MODE_OPER;
                    else
                        operGroupMode = s.MODE_OPER.toLowerCase();
                }
            } else if (mode.contains(s != null ? s.MODE_OWNER.toLowerCase() : "q") && PREFIX.has(s != null ? s.MODE_OWNER : "q")) {
                owners.add(user);
            } else if (mode.contains(s != null ? s.MODE_ADMIN.toLowerCase() : "a") && PREFIX.has(s != null ? s.MODE_ADMIN : "a")) {
                admins.add(user);
            } else if (mode.contains(s != null ? s.MODE_OP.toLowerCase() : "o") && PREFIX.has(s != null ? s.MODE_OP : "o")) {
                ops.add(user);
            } else if (mode.contains(s != null ? s.MODE_HALFOP.toLowerCase() : "h") && PREFIX.has(s != null ? s.MODE_HALFOP : "h")) {
                halfops.add(user);
            } else if (mode.contains(s != null ? s.MODE_VOICED.toLowerCase() : "v") && PREFIX.has(s != null ? s.MODE_VOICED : "v")) {
                voiced.add(user);
            } else {
                members.add(user);
            }
        }

        if (opers.size() > 0) {
            if (showSymbol) {
                if (PREFIX.has(operGroupMode))
                    addUsersFromList(entries, opers, "OPER", PREFIX.get(operGroupMode).asText() + " ", colorScheme.row_opers_bg_drawable, colorScheme.opersHeadingColor);
                else if (PREFIX.has(s != null ? s.MODE_OPER.toLowerCase() : "y"))
                    addUsersFromList(entries, opers, "OPER", PREFIX.get(s != null ? s.MODE_OPER.toLowerCase() : "y").asText() + " ", colorScheme.row_opers_bg_drawable, colorScheme.opersHeadingColor);
                else
                    addUsersFromList(entries, opers, "OPER", "", colorScheme.row_opers_bg_drawable, colorScheme.opersHeadingColor);
            } else {
                addUsersFromList(entries, opers, "OPER", "", colorScheme.row_opers_bg_drawable, colorScheme.opersHeadingColor);
            }
        }

        if (owners.size() > 0) {
            if (showSymbol) {
                if (PREFIX.has(s != null ? s.MODE_OWNER : "q"))
                    addUsersFromList(entries, owners, "OWNER", PREFIX.get(s != null ? s.MODE_OWNER : "q").asText() + " ", colorScheme.row_owners_bg_drawable, colorScheme.ownersHeadingColor);
                else
                    addUsersFromList(entries, owners, "OWNER", "", colorScheme.row_owners_bg_drawable, colorScheme.ownersHeadingColor);
            } else {
                addUsersFromList(entries, owners, "OWNER", "", colorScheme.row_owners_bg_drawable, colorScheme.ownersHeadingColor);
            }
        }

        if (admins.size() > 0) {
            if (showSymbol) {
                if (PREFIX.has(s != null ? s.MODE_ADMIN : "a"))
                    addUsersFromList(entries, admins, "ADMINS", PREFIX.get(s != null ? s.MODE_ADMIN : "a").asText() + " ", colorScheme.row_admins_bg_drawable, colorScheme.adminsHeadingColor);
                else
                    addUsersFromList(entries, admins, "ADMINS", "", colorScheme.row_admins_bg_drawable, colorScheme.adminsHeadingColor);
            } else {
                addUsersFromList(entries, admins, "ADMINS", "", colorScheme.row_admins_bg_drawable, colorScheme.adminsHeadingColor);
            }
        }

        if (ops.size() > 0) {
            if (showSymbol) {
                if (PREFIX.has(s != null ? s.MODE_OP : "o"))
                    addUsersFromList(entries, ops, "OPS", PREFIX.get(s != null ? s.MODE_OP : "o").asText() + " ", colorScheme.row_ops_bg_drawable, colorScheme.opsHeadingColor);
                else
                    addUsersFromList(entries, ops, "OPS", "", colorScheme.row_ops_bg_drawable, colorScheme.opsHeadingColor);
            } else {
                addUsersFromList(entries, ops, "OPS", "", colorScheme.row_ops_bg_drawable, colorScheme.opsHeadingColor);
            }
        }

        if (halfops.size() > 0) {
            if (showSymbol) {
                if (PREFIX.has(s != null ? s.MODE_HALFOP : "h"))
                    addUsersFromList(entries, halfops, "HALF OPS", PREFIX.get(s != null ? s.MODE_HALFOP : "h").asText() + " ", colorScheme.row_halfops_bg_drawable, colorScheme.halfopsHeadingColor);
                else
                    addUsersFromList(entries, halfops, "HALF OPS", "", colorScheme.row_halfops_bg_drawable, colorScheme.halfopsHeadingColor);
            } else {
                addUsersFromList(entries, halfops, "HALF OPS", "", colorScheme.row_halfops_bg_drawable, colorScheme.halfopsHeadingColor);
            }
        }

        if (voiced.size() > 0) {
            if (showSymbol) {
                if (PREFIX.has(s != null ? s.MODE_VOICED : "v"))
                    addUsersFromList(entries, voiced, "VOICED", PREFIX.get(s != null ? s.MODE_VOICED : "v").asText() + " ", colorScheme.row_voiced_bg_drawable, colorScheme.voicedHeadingColor);
                else
                    addUsersFromList(entries, voiced, "VOICED", "", colorScheme.row_voiced_bg_drawable, colorScheme.voicedHeadingColor);
            } else {
                addUsersFromList(entries, voiced, "VOICED", "", colorScheme.row_voiced_bg_drawable, colorScheme.voicedHeadingColor);
            }
        }

        if (members.size() > 0) {
            addUsersFromList(entries, members, "MEMBERS", "", colorScheme.row_members_bg_drawable, colorScheme.membersHeadingColor);
        }

        adapter.setItems(entries);

        if (recyclerView != null && entries.size() > 0 && recyclerView.getAdapter() != adapter)
            recyclerView.setAdapter(adapter);
        else
            adapter.notifyDataSetChanged();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        conn = NetworkConnection.getInstance();
        conn.addHandler(this);
        if (tapTimer == null)
            tapTimer = new Timer("users-tap-timer");

        if (savedInstanceState != null && savedInstanceState.containsKey("cid")) {
            cid = savedInstanceState.getInt("cid");
            bid = savedInstanceState.getInt("bid");
            channel = savedInstanceState.getString("channel");
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.userslist, container);
        recyclerView = v.findViewById(android.R.id.list);
        recyclerView.setLayoutManager(new LinearLayoutManager(v.getContext()));
        return v;
    }

    public void onResume() {
        super.onResume();
        ArrayList<User> users = UsersList.getInstance().getUsersForBuffer(bid);
        refresh(users);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (tapTimer != null) {
            tapTimer.cancel();
            tapTimer = null;
        }
        if (conn != null)
            conn.removeHandler(this);
    }

    @Override
    public void setArguments(Bundle args) {
        if(args != null) {
            if (args.getInt("bid", 0) != bid && recyclerView != null)
                recyclerView.scrollToPosition(0);
            cid = args.getInt("cid", 0);
            bid = args.getInt("bid", 0);
            channel = args.getString("name");
        }

        if (tapTimer == null)
            tapTimer = new Timer("users-tap-timer");

        tapTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                if (getActivity() != null)
                    getActivity().runOnUiThread(new Runnable() {

                        @Override
                        public void run() {
                            ArrayList<User> users = null;
                            if (ChannelsList.getInstance().getChannelForBuffer(bid) != null)
                                users = UsersList.getInstance().getUsersForBuffer(bid);
                            refresh(users);
                            try {
                                if (recyclerView != null)
                                    recyclerView.scrollToPosition(0);
                            } catch (Exception e) { //Sometimes the list view isn't available yet
                            }
                        }

                    });
            }
        }, 100);
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mListener = (OnUserSelectedListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString() + " must implement OnUserSelectedListener");
        }
        if (cid == -1) {
            cid = activity.getIntent().getIntExtra("cid", 0);
            channel = activity.getIntent().getStringExtra("name");
        }
    }

    @Override
    public void onSaveInstanceState(Bundle state) {
        super.onSaveInstanceState(state);
        state.putInt("cid", cid);
        state.putInt("bid", bid);
        state.putString("channel", channel);
    }

    private class OnItemLongClickListener implements OnLongClickListener {
        private int pos;

        OnItemLongClickListener(int position) {
            pos = position;
        }

        @Override
        public boolean onLongClick(View v) {
            if (pos < adapter.getItemCount()) {
                UserListEntry e = (UserListEntry) adapter.getItem(pos);
                if (e.type == TYPE_USER) {
                    mListener.onUserSelected(cid, channel, e.text);
                    return true;
                }
            }
            return false;
        }

    }

    private class OnItemClickListener implements OnClickListener {
        private int pos;

        OnItemClickListener(int position) {
            pos = position;
        }

        @Override
        public void onClick(View arg0) {
            if (pos < 0)
                return;

            if (adapter != null && tapTimer != null) {
                if (tapTimerTask != null) {
                    tapTimerTask.cancel();
                    tapTimerTask = null;
                    try {
                        UserListEntry e = (UserListEntry) adapter.getItem(pos);
                        if (e.type == TYPE_USER)
                            mListener.onUserDoubleClicked(UsersList.getInstance().getUser(bid,e.text).nick);
                    } catch (Exception e) {

                    }
                } else {
                    tapTimerTask = new TimerTask() {
                        int position = pos;

                        @Override
                        public void run() {
                            if (getActivity() != null)
                                getActivity().runOnUiThread(new Runnable() {
                                    @Override
                                    public void run() {
                                        try {
                                            UserListEntry e = (UserListEntry) adapter.getItem(position);
                                            if (e.type == TYPE_USER) {
                                                mListener.onUserSelected(cid, channel, e.text);

                                                if (!getActivity().getSharedPreferences("prefs", 0).getBoolean("longPressTip", false)) {
                                                    Toast.makeText(getActivity(), "Long-press a message to quickly interact with the sender", Toast.LENGTH_LONG).show();
                                                    SharedPreferences.Editor editor = getActivity().getSharedPreferences("prefs", 0).edit();
                                                    editor.putBoolean("longPressTip", true);
                                                    editor.apply();
                                                }
                                            }
                                        } catch (Exception e) {

                                        }
                                    }
                                });
                            tapTimerTask = null;
                        }
                    };
                    tapTimer.schedule(tapTimerTask, 300);
                }
            }
        }
    }

    public void onIRCEvent(int what, Object obj) {
        switch (what) {
            case NetworkConnection.EVENT_CONNECTIVITY:
                if (adapter != null && getActivity() != null) {
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            if (adapter != null)
                                adapter.notifyDataSetChanged();
                        }
                    });
                }
                break;
            case NetworkConnection.EVENT_AWAY:
                if (((IRCCloudJSONObject) obj).cid() == cid) {
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            if (adapter != null)
                                adapter.notifyDataSetChanged();
                        }
                    });
                }
                break;
            case NetworkConnection.EVENT_JOIN:
            case NetworkConnection.EVENT_PART:
            case NetworkConnection.EVENT_QUIT:
            case NetworkConnection.EVENT_USERCHANNELMODE:
            case NetworkConnection.EVENT_KICK:
            case NetworkConnection.EVENT_NICKCHANGE:
                if (what == NetworkConnection.EVENT_NICKCHANGE && ((IRCCloudJSONObject) obj).bid() != bid)
                    break;
            case NetworkConnection.EVENT_DISPLAYNAMECHANGE:
                if (what == NetworkConnection.EVENT_DISPLAYNAMECHANGE && ((IRCCloudJSONObject) obj).cid() != cid)
                    break;
            case NetworkConnection.EVENT_CHANNELINIT:
            case NetworkConnection.EVENT_USERINFO:
            case NetworkConnection.EVENT_MEMBERUPDATES:
            case NetworkConnection.EVENT_BACKLOG_END:
                if (getActivity() != null) {
                    final ArrayList<User> users;
                    if (ChannelsList.getInstance().getChannelForBuffer(bid) != null) {
                        users = UsersList.getInstance().getUsersForBuffer(bid);
                        getActivity().runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                refresh(users);
                            }
                        });
                    }
                }
                break;
            default:
                break;
        }
    }

    public interface OnUserSelectedListener {
        void onUserSelected(int cid, String channel, String name);
        void onUserDoubleClicked(String name);
    }
} public class WhoListFragment extends DialogFragment {
    JsonNode users;
    UsersAdapter adapter;
    NetworkConnection conn;
    RecyclerView recyclerView;
    TextView empty;
    IRCCloudJSONObject event;

    private class ViewHolder extends RecyclerView.ViewHolder {
        public RowWhoBinding binding;

        public ViewHolder(View v) {
            super(v);
            binding = DataBindingUtil.bind(v);
        }
    }

    private class UsersAdapter extends RecyclerView.Adapter<ViewHolder> {

        @Override
        public int getItemCount() {
            return users.size();
        }

        @Override
        public long getItemId(int pos) {
            return pos;
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = RowWhoBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false).getRoot();
            return new ViewHolder(v);
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, final int position) {
            RowWhoBinding row = holder.binding;
            row.setNick(users.get(position).get("nick").asText());
            row.setName(ColorFormatter.html_to_spanned("&nbsp;(" + ColorFormatter.emojify(ColorFormatter.irc_to_html(TextUtils.htmlEncode(users.get(position).get("realname").asText()))) + ")"));
            row.setServer("Connected via " + users.get(position).get("ircserver").asText());
            row.setMask(ColorFormatter.html_to_spanned(ColorFormatter.irc_to_html(users.get(position).get("usermask").asText())));
            row.getRoot().setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View view) {
                    final JsonNode user = users.get(position);
                    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

                    builder.setItems(new String[]{"Send a message", "Whois…", "Copy hostmask"}, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            switch (i) {
                                case 0:
                                    dismiss();
                                    conn.say(event.cid(), null, "/query " + user.get("nick").asText(), null);
                                    break;
                                case 1:
                                    dismiss();
                                    conn.say(event.cid(), null, "/whois " + user.get("nick").asText(), null);
                                    break;
                                case 2:
                                    android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getActivity().getSystemService(Activity.CLIPBOARD_SERVICE);
                                    android.content.ClipData clip = android.content.ClipData.newPlainText("Hostmask", user.get("nick").asText() + "!" + user.get("usermask").asText());
                                    clipboard.setPrimaryClip(clip);
                                    Toast.makeText(getActivity(), "Hostmask copied to clipboard", Toast.LENGTH_SHORT).show();
                                    break;
                            }
                        }
                    });

                    AlertDialog dialog = builder.create();
                    dialog.setOwnerActivity(getActivity());
                    dialog.show();
                    return false;
                }
            });
            row.executePendingBindings();
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Context ctx = getActivity();
        if(ctx == null)
            return null;

        LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(R.layout.recyclerview, null);
        recyclerView = v.findViewById(R.id.recycler);
        recyclerView.setLayoutManager(new LinearLayoutManager(v.getContext()));
        empty = v.findViewById(android.R.id.empty);
        empty.setText("No results found.");
        if (savedInstanceState != null && savedInstanceState.containsKey("event")) {
            event = new IRCCloudJSONObject(savedInstanceState.getString("event"));
            users = event.getJsonNode("users");
            adapter = new UsersAdapter();
            recyclerView.setAdapter(adapter);
            if(adapter.getItemCount() > 0) {
                empty.setVisibility(View.GONE);
                recyclerView.setVisibility(View.VISIBLE);
            } else {
                empty.setVisibility(View.VISIBLE);
                recyclerView.setVisibility(View.GONE);
            }
        }
        Dialog d = new AlertDialog.Builder(ctx)
                .setTitle("WHO response for " + event.getString("subject"))
                .setView(v)
                .setNegativeButton("Close", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                })
                .create();
        return d;
    }

    @Override
    public void onSaveInstanceState(Bundle state) {
        state.putString("event", event.toString());
    }

    @Override
    public void setArguments(Bundle args) {
        if(args != null) {
            event = new IRCCloudJSONObject(args.getString("event"));
            users = event.getJsonNode("users");
            if (getActivity() != null && recyclerView != null) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (adapter == null) {
                            adapter = new UsersAdapter();
                            recyclerView.setAdapter(adapter);
                        } else {
                            adapter.notifyDataSetChanged();
                        }
                        if (adapter.getItemCount() > 0) {
                            empty.setVisibility(View.GONE);
                            recyclerView.setVisibility(View.VISIBLE);
                        } else {
                            empty.setVisibility(View.VISIBLE);
                            recyclerView.setVisibility(View.GONE);
                        }
                    }
                });
            }
        }
    }

    public void onResume() {
        super.onResume();
        conn = NetworkConnection.getInstance();

        if (users != null) {
            adapter = new UsersAdapter();
            recyclerView.setAdapter(adapter);
            if(adapter.getItemCount() > 0) {
                empty.setVisibility(View.GONE);
                recyclerView.setVisibility(View.VISIBLE);
            } else {
                empty.setVisibility(View.VISIBLE);
                recyclerView.setVisibility(View.GONE);
            }
        }
    }

    @Override
    public void onPause() {
        super.onPause();
    }
} public class WhoWasFragment extends DialogFragment {
    JsonNode data;
    IRCCloudJSONObject event;
    Adapter adapter;
    NetworkConnection conn;
    RecyclerView recyclerView;
    TextView empty;
    String title;

    private class ViewHolder extends RecyclerView.ViewHolder {
        public RowWhowasBinding binding;

        public ViewHolder(View v) {
            super(v);
            binding = DataBindingUtil.bind(v);
        }
    }

    private class Adapter extends RecyclerView.Adapter<ViewHolder> {

        @Override
        public int getItemCount() {
            if(data.size() == 1 && data.get(0).has("no_such_nick"))
                return 0;
            else
                return data.size();
        }

        @Override
        public long getItemId(int pos) {
            return pos;
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = RowWhowasBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false).getRoot();
            return new ViewHolder(v);
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, final int position) {
            RowWhowasBinding row = holder.binding;
            JsonNode node = data.get(position);
            row.setNick(node.get("nick").asText());
            if(node.has("user") && node.has("host")) {
                row.setUsermask(node.get("user").asText() + "@" + node.get("host").asText());
            } else {
                row.usermaskHeading.setVisibility(View.GONE);
                row.usermask.setVisibility(View.GONE);
            }
            row.setRealname(node.get("realname").asText());
            if(node.get("last_seen") != null)
                row.setLastconnected(node.get("last_seen").asText());
            else
                row.setLastconnected("");
            if(node.has("ircserver") && node.get("ircserver") != null) {
                row.setConnectedvia(node.get("ircserver").asText());
            } else {
                row.setConnectedvia("");
            }
            if(node.has("connecting_from") && node.get("connecting_from") != null) {
                row.setInfo(node.get("connecting_from").asText());
            } else {
                row.infoHeading.setVisibility(View.GONE);
                row.info.setVisibility(View.GONE);
            }
            if(node.has("actual_host") && node.get("actual_host") != null) {
                row.setActualhost(node.get("actual_host").asText());
            } else {
                row.actualhostHeading.setVisibility(View.GONE);
                row.actualhost.setVisibility(View.GONE);
            }
            row.executePendingBindings();
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Context ctx = getActivity();
        if (ctx == null)
            return null;

        LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(R.layout.recyclerview, null);
        recyclerView = v.findViewById(R.id.recycler);
        recyclerView.setLayoutManager(new LinearLayoutManager(v.getContext()));
        empty = v.findViewById(android.R.id.empty);
        empty.setText("There was no such nickname");
        if (savedInstanceState != null && savedInstanceState.containsKey("event")) {
            event = new IRCCloudJSONObject(savedInstanceState.getString("event"));
            title = savedInstanceState.getString("title");
            data = event.getJsonNode("lines");
            adapter = new Adapter();
            recyclerView.setAdapter(adapter);
            if(adapter.getItemCount() > 0) {
                empty.setVisibility(View.GONE);
                recyclerView.setVisibility(View.VISIBLE);
            } else {
                empty.setVisibility(View.VISIBLE);
                recyclerView.setVisibility(View.GONE);
            }
        }
        AlertDialog.Builder b = new AlertDialog.Builder(ctx)
                .setTitle(title)
                .setView(v)
                .setNegativeButton("Close", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                });

        AlertDialog d = b.create();
        d.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
        return d;
    }

    @Override
    public void onSaveInstanceState(Bundle state) {
        state.putString("event", event.toString());
        state.putString("title", title);
    }

    @Override
    public void setArguments(Bundle args) {
        if(args != null) {
            event = new IRCCloudJSONObject(args.getString("event"));
            title = args.getString("title");
            data = event.getJsonNode("lines");

            if (getActivity() != null && recyclerView != null) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (adapter == null) {
                            adapter = new Adapter();
                            recyclerView.setAdapter(adapter);
                        } else {
                            adapter.notifyDataSetChanged();
                        }
                        if (adapter.getItemCount() > 0) {
                            empty.setVisibility(View.GONE);
                            recyclerView.setVisibility(View.VISIBLE);
                        } else {
                            empty.setVisibility(View.VISIBLE);
                            recyclerView.setVisibility(View.GONE);
                        }
                    }
                });
            }
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        adapter = new Adapter();
        recyclerView.setAdapter(adapter);
        if(adapter.getItemCount() > 0) {
            empty.setVisibility(View.GONE);
            recyclerView.setVisibility(View.VISIBLE);
        } else {
            empty.setVisibility(View.VISIBLE);
            recyclerView.setVisibility(View.GONE);
        }
    }
} public class WhoisFragment extends DialogFragment {
    IRCCloudJSONObject event;
    TextView extra, name, mask, serverTitle, server, time, timeTitle, channels, channelsTitle, opChannels, opTitle,
            operChannels, operTitle, ownerChannels, ownerTitle, adminChannels, adminTitle, halfopChannels, halfopTitle, voicedChannels, voicedTitle, awayTitle, away;

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Context ctx = getActivity();

        if (ctx == null)
            return null;

        LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(R.layout.dialog_whois, null);
        extra = v.findViewById(R.id.extra);
        name = v.findViewById(R.id.name);
        mask = v.findViewById(R.id.mask);
        serverTitle = v.findViewById(R.id.serverTitle);
        server = v.findViewById(R.id.server);
        timeTitle = v.findViewById(R.id.timeTitle);
        time = v.findViewById(R.id.time);
        awayTitle = v.findViewById(R.id.awayTitle);
        away = v.findViewById(R.id.away);
        operTitle = v.findViewById(R.id.operTitle);
        operChannels = v.findViewById(R.id.operChannels);
        operChannels.setMovementMethod(IRCCloudLinkMovementMethod.getInstance());
        operChannels.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                getDialog().dismiss();
            }
        });
        ownerTitle = v.findViewById(R.id.ownerTitle);
        ownerChannels = v.findViewById(R.id.ownerChannels);
        ownerChannels.setMovementMethod(IRCCloudLinkMovementMethod.getInstance());
        ownerChannels.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                getDialog().dismiss();
            }
        });
        adminTitle = v.findViewById(R.id.adminTitle);
        adminChannels = v.findViewById(R.id.adminChannels);
        adminChannels.setMovementMethod(IRCCloudLinkMovementMethod.getInstance());
        adminChannels.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                getDialog().dismiss();
            }
        });
        opTitle = v.findViewById(R.id.opTitle);
        opChannels = v.findViewById(R.id.opChannels);
        opChannels.setMovementMethod(IRCCloudLinkMovementMethod.getInstance());
        opChannels.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                getDialog().dismiss();
            }
        });
        halfopTitle = v.findViewById(R.id.halfopTitle);
        halfopChannels = v.findViewById(R.id.halfopChannels);
        halfopChannels.setMovementMethod(IRCCloudLinkMovementMethod.getInstance());
        halfopChannels.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                getDialog().dismiss();
            }
        });
        voicedTitle = v.findViewById(R.id.voicedTitle);
        voicedChannels = v.findViewById(R.id.voicedChannels);
        voicedChannels.setMovementMethod(IRCCloudLinkMovementMethod.getInstance());
        voicedChannels.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                getDialog().dismiss();
            }
        });
        channelsTitle = v.findViewById(R.id.channelsTitle);
        channels = v.findViewById(R.id.channels);
        channels.setMovementMethod(IRCCloudLinkMovementMethod.getInstance());
        channels.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                getDialog().dismiss();
            }
        });
        if (savedInstanceState != null && savedInstanceState.containsKey("event")) {
            event = new IRCCloudJSONObject(savedInstanceState.getString("event"));
        }
        Dialog d = new AlertDialog.Builder(ctx)
                .setTitle("WHOIS response for " + event.getString("user_nick"))
                .setView(v)
                .setNegativeButton("Close", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                })
                .create();
        return d;
    }

    @Override
    public void onSaveInstanceState(Bundle state) {
        state.putString("event", event.toString());
    }

    @Override
    public void setArguments(Bundle args) {
        if(args != null)
            event = new IRCCloudJSONObject(args.getString("event"));
    }

    private String format_duration(long duration) {
        if (duration > (86400 * 30)) {
            return ((int) (duration / (86400 * 30))) + " months";
        } else if (duration > 86400) {
            return ((int) (duration / 86400)) + " days";
        } else if (duration > 3600) {
            return ((int) (duration / 3600)) + " hours";
        } else if (duration > 60) {
            return ((int) (duration / 60)) + " minutes";
        } else {
            return duration + " seconds";
        }
    }

    public void onResume() {
        super.onResume();

        if (event != null) {
            String nick = event.getString("user_nick");
            String extratxt = "";
            if (event.has("op_nick")) {
                extratxt += nick + " " + event.getString("op_msg") + "\n\n";
            }
            if (event.has("opername")) {
                extratxt += nick + " " + event.getString("opername_msg") + " " + event.getString("opername") + "\n\n";
            }
            if (event.has("stats_dline")) {
                extratxt += nick + " " + event.getString("stats_dline") + "\n\n";
            }
            if (event.has("userip")) {
                extratxt += nick + " " + event.getString("userip") + "\n\n";
            }
            if (event.has("host")) {
                extratxt += nick + " " + event.getString("host") + "\n\n";
            }
            if (event.has("bot_msg")) {
                extratxt += nick + " " + event.getString("bot_msg") + "\n\n";
            }
            if (event.has("cgi")) {
                extratxt += nick + " " + event.getString("cgi") + "\n\n";
            }
            if (event.has("help")) {
                extratxt += nick + " " + event.getString("help") + "\n\n";
            }
            if (event.has("vworld")) {
                extratxt += nick + " " + event.getString("vworld") + "\n\n";
            }
            if (event.has("modes")) {
                extratxt += nick + " " + event.getString("modes") + "\n\n";
            }
            if (event.has("client_cert")) {
                extratxt += nick + " " + event.getString("client_cert") + "\n\n";
            }
            if (event.has("secure")) {
                extratxt += nick + " " + event.getString("secure") + "\n\n";
            }
            if (extratxt.length() > 0) {
                extra.setVisibility(View.VISIBLE);
                extra.setText(extratxt.substring(0, extratxt.length() - 2));
            } else {
                extra.setVisibility(View.GONE);
            }
            if (event.has("user_realname")) {
                String nametxt = event.getString("user_realname");
                if (event.has("user_logged_in_as") && event.getString("user_logged_in_as").length() > 0) {
                    nametxt += "\u000F (authed as " + event.getString("user_logged_in_as") + ")";
                }
                name.setText(ColorFormatter.html_to_spanned(ColorFormatter.emojify(ColorFormatter.irc_to_html(TextUtils.htmlEncode(nametxt)))));
            } else {
                name.setText("");
            }
            if(event.has("user_mask")) {
                mask.setText(ColorFormatter.html_to_spanned(ColorFormatter.irc_to_html(TextUtils.htmlEncode(event.getString("user_mask")))));
                if (event.has("actual_host"))
                    mask.setText(mask.getText() + "/" + event.getString("actual_host"));
            } else {
                mask.setVisibility(View.GONE);
            }
            if(event.has("server_addr") && event.getString("server_addr") != null && event.getString("server_addr").length() > 0) {
                String s = event.getString("server_addr");
                if (event.has("server_extra") && event.getString("server_extra").length() > 0)
                    s += " (" + event.getString("server_extra") + ")";
                server.setText(s);
            } else {
                server.setVisibility(View.GONE);
                serverTitle.setVisibility(View.GONE);
            }
            if (event.has("signon_time")) {
                String timetxt = format_duration((System.currentTimeMillis() / 1000L) - event.getLong("signon_time"));
                if (event.has("idle_secs") && event.getLong("idle_secs") > 0)
                    timetxt += " (idle for " + format_duration(event.getLong("idle_secs")) + ")";
                time.setText(timetxt);
                timeTitle.setVisibility(View.VISIBLE);
                time.setVisibility(View.VISIBLE);
            } else {
                timeTitle.setVisibility(View.GONE);
                time.setVisibility(View.GONE);
            }
            if (event.has("away") && event.getString("away").length() > 0) {
                away.setText(ColorFormatter.html_to_spanned(ColorFormatter.emojify(ColorFormatter.irc_to_html(TextUtils.htmlEncode(event.getString("away"))))));
                awayTitle.setVisibility(View.VISIBLE);
                away.setVisibility(View.VISIBLE);
            } else {
                awayTitle.setVisibility(View.GONE);
                away.setVisibility(View.GONE);
            }
            buildChannelList("channels_oper", operTitle, operChannels);
            buildChannelList("channels_owner", ownerTitle, ownerChannels);
            buildChannelList("channels_admin", adminTitle, adminChannels);
            buildChannelList("channels_op", opTitle, opChannels);
            buildChannelList("channels_halfop", halfopTitle, halfopChannels);
            buildChannelList("channels_voiced", voicedTitle, voicedChannels);
            buildChannelList("channels_member", channelsTitle, channels);
        }
    }

    private void buildChannelList(String field, TextView title, TextView channels) {
        if (event.has(field)) {
            title.setVisibility(View.VISIBLE);
            channels.setVisibility(View.VISIBLE);
            String channelstxt = "";
            JsonNode c = event.getJsonNode(field);
            for (int i = 0; i < c.size(); i++) {
                String chan = c.get(i).asText();
                channelstxt += "• " + chan + "<br/>";
            }
            channels.setText(ColorFormatter.html_to_spanned(channelstxt, true, ServersList.getInstance().getServer(event.cid())));
        } else {
            title.setVisibility(View.GONE);
            channels.setVisibility(View.GONE);
        }
    }
} public class CustomTabsHelper {
    private static final String TAG = "CustomTabsHelper";
    static final String STABLE_PACKAGE = "com.android.chrome";
    static final String BETA_PACKAGE = "com.chrome.beta";
    static final String DEV_PACKAGE = "com.chrome.dev";
    static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
    private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE =
            "android.support.customtabs.extra.KEEP_ALIVE";
    private static final String ACTION_CUSTOM_TABS_CONNECTION =
            "android.support.customtabs.action.CustomTabsService";

    private static String sPackageNameToUse;

    private CustomTabsHelper() {}

    /**
     * Goes through all apps that handle VIEW intents and have a warmup service. Picks
     * the one chosen by the user if there is one, otherwise makes a best effort to return a
     * valid package name.
     *
     * This is <strong>not</strong> threadsafe.
     *
     * @param context {@link Context} to use for accessing {@link PackageManager}.
     * @return The package name recommended to use for connecting to custom tabs related components.
     */
    public static String getPackageNameToUse(Context context) {
        if (sPackageNameToUse != null) return sPackageNameToUse;

        PackageManager pm = context.getPackageManager();
        // Get default VIEW intent handler.
        Intent activityIntent = new Intent()
                .setAction(Intent.ACTION_VIEW)
                .addCategory(Intent.CATEGORY_BROWSABLE)
                .setData(Uri.fromParts("http", "", null));

        ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0);
        String defaultViewHandlerPackageName = null;
        if (defaultViewHandlerInfo != null) {
            defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName;
        }

        // Get all apps that can handle VIEW intents.
        List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, 0);
        ArrayList<String> packagesSupportingCustomTabs = new ArrayList<>();
        for (ResolveInfo info : resolvedActivityList) {
            Intent serviceIntent = new Intent();
            serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION);
            serviceIntent.setPackage(info.activityInfo.packageName);
            // Check if this package also resolves the Custom Tabs service.
            if (pm.resolveService(serviceIntent, 0) != null) {
                packagesSupportingCustomTabs.add(info.activityInfo.packageName);
            }
        }

        // Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents
        // and service calls.
        if (packagesSupportingCustomTabs.isEmpty()) {
            sPackageNameToUse = null;
        } else if (packagesSupportingCustomTabs.size() == 1) {
            sPackageNameToUse = packagesSupportingCustomTabs.get(0);
        } else if (!TextUtils.isEmpty(defaultViewHandlerPackageName)
                && !hasSpecializedHandlerIntents(context, activityIntent)
                && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) {
            sPackageNameToUse = defaultViewHandlerPackageName;
        } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
            sPackageNameToUse = STABLE_PACKAGE;
        } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
            sPackageNameToUse = BETA_PACKAGE;
        } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
            sPackageNameToUse = DEV_PACKAGE;
        } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
            sPackageNameToUse = LOCAL_PACKAGE;
        }
        return sPackageNameToUse;
    }

    /**
     * Used to check whether there is a specialized handler for a given intent.
     * @param intent The intent to check with.
     * @return Whether there is a specialized handler for the given intent.
     */
    private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) {
        try {
            PackageManager pm = context.getPackageManager();
            List<ResolveInfo> handlers = pm.queryIntentActivities(
                    intent,
                    PackageManager.GET_RESOLVED_FILTER);
            if (handlers == null || handlers.size() == 0) {
                return false;
            }
            for (ResolveInfo resolveInfo : handlers) {
                IntentFilter filter = resolveInfo.filter;
                if (filter == null) continue;
                if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue;
                if (resolveInfo.activityInfo == null) continue;
                return true;
            }
        } catch (RuntimeException e) {
            Log.e(TAG, "Runtime exception while getting specialized handlers");
        }
        return false;
    }

    /**
     * @return All possible chrome package names that provide custom tabs feature.
     */
    public static String[] getPackages() {
        return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE};
    }
} public static final int ROW_FILE = 6; public static final int ROW_THUMBNAIL = 5;
import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.content.res.Configuration;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Debug;
import android.os.Environment;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.text.Editable;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.URLSpan;
import android.text.style.UnderlineSpan;
import android.util.Log;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.ActionMode;
import android.view.DragEvent;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.KeyboardShortcutGroup;
import android.view.KeyboardShortcutInfo;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener;
import android.view.View.OnKeyListener;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.webkit.MimeTypeMap;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.GridView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsServiceConnection;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.ShareCompat;
import androidx.core.content.FileProvider;
import androidx.core.graphics.drawable.IconCompat;
import androidx.core.view.inputmethod.InputContentInfoCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.exifinterface.media.ExifInterface;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import com.commonsware.cwac.richedit.Effect;
import com.commonsware.cwac.richedit.RichEditText;
import com.damnhandy.uri.template.UriTemplate;
import com.datatheorem.android.trustkit.TrustKit;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.firebase.analytics.FirebaseAnalytics;
import com.google.firebase.messaging.FirebaseMessaging;
import com.irccloud.android.ActionEditText;
import com.irccloud.android.AsyncTaskEx;
import com.irccloud.android.BackgroundTaskWorker;
import com.irccloud.android.BuildConfig;
import com.irccloud.android.CollapsedEventsList;
import com.irccloud.android.ColorFormatter;
import com.irccloud.android.ColorScheme;
import com.irccloud.android.FontAwesome;
import com.irccloud.android.IRCCloudApplication;
import com.irccloud.android.IRCCloudJSONObject;
import com.irccloud.android.IRCCloudLinkMovementMethod;
import com.irccloud.android.IRCCloudLog;
import com.irccloud.android.IRCEditText;
import com.irccloud.android.NetworkConnection;
import com.irccloud.android.R;
import com.irccloud.android.data.collection.AvatarsList;
import com.irccloud.android.data.collection.BuffersList;
import com.irccloud.android.data.collection.ChannelsList;
import com.irccloud.android.data.collection.EventsList;
import com.irccloud.android.data.collection.ImageList;
import com.irccloud.android.data.collection.NotificationsList;
import com.irccloud.android.data.collection.RecentConversationsList;
import com.irccloud.android.data.collection.ServersList;
import com.irccloud.android.data.collection.UsersList;
import com.irccloud.android.data.model.Buffer;
import com.irccloud.android.data.model.Channel;
import com.irccloud.android.data.model.Event;
import com.irccloud.android.data.model.Server;
import com.irccloud.android.data.model.User;
import com.irccloud.android.fragment.AcceptListFragment;
import com.irccloud.android.fragment.BufferOptionsFragment;
import com.irccloud.android.fragment.BuffersListFragment;
import com.irccloud.android.fragment.ChannelListFragment;
import com.irccloud.android.fragment.ChannelModeListFragment;
import com.irccloud.android.fragment.EditConnectionFragment;
import com.irccloud.android.fragment.IRCColorPickerFragment;
import com.irccloud.android.fragment.IgnoreListFragment;
import com.irccloud.android.fragment.LinksListFragment;
import com.irccloud.android.fragment.MessageViewFragment;
import com.irccloud.android.fragment.NamesListFragment;
import com.irccloud.android.fragment.NickservFragment;
import com.irccloud.android.fragment.PinReorderFragment;
import com.irccloud.android.fragment.ServerReorderFragment;
import com.irccloud.android.fragment.SpamFragment;
import com.irccloud.android.fragment.TextListFragment;
import com.irccloud.android.fragment.UsersListFragment;
import com.irccloud.android.fragment.WhoListFragment;
import com.irccloud.android.fragment.WhoWasFragment;
import com.irccloud.android.fragment.WhoisFragment;
import org.chromium.customtabsclient.shared.CustomTabsHelper;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import javax.net.ssl.HttpsURLConnection;
import static com.irccloud.android.fragment.MessageViewFragment.ROW_FILE;
import static com.irccloud.android.fragment.MessageViewFragment.ROW_THUMBNAIL;
            if(url != null && url.length() > 0) {
                if (message != null && message.length() > 0)
                    text += message + " ";
                text += url;
            } else {
                text = pastebinResult.getStringExtra("paste_contents");
            }
            messageTxt.setText(text);
            SendTask t = new SendTask();
            t.forceText = true;
            runOnUiThread(t);
            pastebinResult = null;
        }

        try {
            if (NetworkConnection.getInstance().session != null && NetworkConnection.getInstance().session.length() > 0) {
                FirebaseMessaging.getInstance().getToken().addOnSuccessListener(new OnSuccessListener<String>() {
                    @Override
                    public void onSuccess(@NonNull String s) {
                        GcmTask t = new GcmTask();
                        t.token = s;
                        t.execute((Void) null);
                    }
                });
            }
        } catch (Exception e) {

        }
        sendBtn.setEnabled(conn.getState() == NetworkConnection.STATE_CONNECTED && messageTxt.getText().length() > 0);
        photoBtn.setVisibility(bubble ? View.GONE : View.VISIBLE);
        IRCCloudLinkMovementMethod.bubble = bubble;
    }

    CustomTabsServiceConnection mCustomTabsConnection = new CustomTabsServiceConnection() {
        @Override
        public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
            try {
                if(client != null)
                    client.warmup(0);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    @Override
    protected void onStart() {
        super.onStart();

        try {
            String packageName = CustomTabsHelper.getPackageNameToUse(this);
            if (packageName != null && packageName.length() > 0)
                CustomTabsClient.bindCustomTabsService(this, packageName, mCustomTabsConnection);
        } catch (Exception e) {
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (imgurTask != null)
            imgurTask.setActivity(null);
        if (fileUploadTask != null)
            fileUploadTask.setActivity(null);
        try {
            if (excludeBIDTask != null)
                excludeBIDTask.cancel(true);
        } catch (Exception e) {
        }
        ExcludeBIDTask t = new ExcludeBIDTask();
        t.execute(-1);
        try {
            unbindService(mCustomTabsConnection);
        } catch (Exception e) {
        }
    }

    @Override
    public void onPause() {
        super.onPause();

        if(buffer != null && buffer.getBid() > 0 && messageTxt != null)
            buffer.setDraft(messageTxt.getText());

        if(isFinishing()) {
            if (imgurTask != null)
                imgurTask.setActivity(null);
            if (fileUploadTask != null)
                fileUploadTask.setActivity(null);

            if (shouldMarkAsRead()) {
                Long eid = EventsList.getInstance().lastEidForBuffer(buffer.getBid());
                if (eid >= buffer.getLast_seen_eid() && conn != null && conn.getState() == NetworkConnection.STATE_CONNECTED) {
                    NetworkConnection.getInstance().heartbeat(buffer.getCid(), buffer.getBid(), eid, null);
                    buffer.setLast_seen_eid(eid);
                    buffer.setUnread(0);
                    buffer.setHighlights(0);
                }
            }

            try {
                if (excludeBIDTask != null)
                    excludeBIDTask.cancel(true);
            } catch (Exception e) {
            }
            excludeBIDTask = new ExcludeBIDTask();
            excludeBIDTask.execute(-1);
            suggestionsAdapter.clear();
            progressBar.setVisibility(View.GONE);
            errorMsg.setVisibility(View.GONE);
            error = null;
            synchronized (backStack) {
                if (buffer != null && !bubble)
                    backStack.add(0, buffer.getBid());
            }
            ImageList.getInstance().clearFailures();