Skip to main content

Overview

Threadly’s Stories feature allows users to share photos and videos that disappear after 24 hours. Stories appear in a horizontal scrollable feed at the top of the home screen.

Stories Manager

The StoriesManager handles all story-related operations:
network_managers/StoriesManager.java
public class StoriesManager {
    SharedPreferences loginInfo;
    
    public StoriesManager(){
        this.loginInfo = Core.getPreference();
    }
    
    // Add a new story
    public void AddStory(File media, String Type, 
                        NetworkCallbackInterfaceWithProgressTracking callback)
    
    // Get stories from followed users
    public void getStories(NetworkCallbackInterfaceWithJsonObjectDelivery callback)
    
    // Get stories of a specific user
    public void getStoriesOf(String Userid, 
                            NetworkCallbackInterfaceWithJsonObjectDelivery callback)
    
    // Get my own stories
    public void getMyStories(NetworkCallbackInterfaceWithJsonObjectDelivery callback)
    
    // Delete a story
    public void RemoveStory(int storyId, NetworkCallbackInterface callback)
}

Adding Stories

Upload with Progress Tracking

network_managers/StoriesManager.java
public void AddStory(File media, String Type, 
                    NetworkCallbackInterfaceWithProgressTracking callbackInterface){
    String Url = ApiEndPoints.ADD_STORY;
    AndroidNetworking.upload(Url)
        .addHeaders("Authorization", "Bearer " + 
            loginInfo.getString(SharedPreferencesKeys.JWT_TOKEN, "null"))
        .addMultipartFile("media", media)
        .addMultipartParameter("type", Type)
        .setPriority(Priority.HIGH)
        .build()
        .setUploadProgressListener(new UploadProgressListener() {
            @Override
            public void onProgress(long bytesUploaded, long totalBytes) {
                callbackInterface.progress(bytesUploaded, totalBytes);
            }
        })
        .getAsJSONObject(new JSONObjectRequestListener() {
            @Override
            public void onResponse(JSONObject response) {
                callbackInterface.onSuccess(response);
            }
            
            @Override
            public void onError(ANError anError) {
                callbackInterface.onError(anError.getMessage());
            }
        });
}

Story Upload Activity

Users create stories through AddStoryActivity:
activities/AddStoryActivity.java
// After capturing or selecting media
File mediaFile = new File(mediaPath);
String type = isImage ? "image" : "video";

StoriesManager storiesManager = new StoriesManager();
storiesManager.AddStory(mediaFile, type, 
    new NetworkCallbackInterfaceWithProgressTracking() {
    @Override
    public void onSuccess(JSONObject response) {
        // Story uploaded successfully
        showUploadProgressNotification(0, 0, false, true, notificationCode);
        mediaFile.delete();
        finish();
    }
    
    @Override
    public void onError(String err) {
        // Upload failed
        Toast.makeText(this, "Failed to upload story", Toast.LENGTH_SHORT).show();
    }
    
    @Override
    public void progress(long bytesUploaded, long totalBytes) {
        // Update progress UI
        int progress = (int) ((bytesUploaded * 100) / totalBytes);
        progressBar.setProgress(progress);
    }
});

Viewing Stories

Stories Feed Display

Stories appear in a horizontal RecyclerView at the top of the home feed:
fragments/homeFragment.java
// Setup stories RecyclerView
LinearLayoutManager layoutManager = 
    new LinearLayoutManager(requireActivity(), LinearLayoutManager.HORIZONTAL, false);
StatusViewAdapter StoriesAdapter = new StatusViewAdapter(
    requireActivity(), 
    storiesData, 
    (userid, profilePic, list, position) -> 
        callback.openStoryOf(userid, profilePic, list, position)
);
mainXml.storyRecyclerView.setLayoutManager(layoutManager);
mainXml.storyRecyclerView.setAdapter(StoriesAdapter);

Loading Stories

storiesViewModel.getStories().observe(getViewLifecycleOwner(), storiesModels -> {
    if(storiesModels.isEmpty()){
        mainXml.storiesShimmer.setVisibility(View.GONE);
    } else {
        storiesData.clear();
        storiesData.addAll(storiesModels);
        StoriesAdapter.notifyDataSetChanged();
        mainXml.storiesShimmer.setVisibility(View.GONE);
        mainXml.storyRecyclerView.setVisibility(View.VISIBLE);
    }
});

My Stories

Users can view and manage their own stories:
fragments/homeFragment.java
// Load my stories
storiesViewModel.getMyStories().observe(getViewLifecycleOwner(), storyMediaModels -> {
    if(!storyMediaModels.isEmpty()){
        // User has active stories - show colored ring
        mainXml.StoryOuterBorderColor.setBackground(
            AppCompatResources.getDrawable(requireActivity(), R.drawable.red_circle)
        );
        mainXml.addStorySymbol.setVisibility(View.GONE);
        mainXml.MyStoryUsername.setText(R.string.your_story);
        mainXml.myStoryLayoutMain.setVisibility(View.VISIBLE);
    } else {
        // No stories - show add button
        mainXml.myStoryLayoutMain.setVisibility(View.VISIBLE);
        mainXml.addStorySymbol.setVisibility(View.VISIBLE);
    }
});

My Story Click Handler

private void setMyStoryClickCallback(String userid, String profile) {
    mainXml.myStoryLayoutMain.setOnClickListener(v -> {
        if(mainXml.addStorySymbol.getVisibility() == View.VISIBLE){
            // Add new story
            Intent intent = new Intent(requireActivity(), AddStoryActivity.class);
            intent.putExtra("title", "New Story");
            startActivity(intent);
        } else {
            // View my stories
            callback.openStoryOf(userid, profile, new ArrayList<>(), 0);
        }
    });
}

Fetching Stories

Get All Stories

network_managers/StoriesManager.java
public void getStories(
    NetworkCallbackInterfaceWithJsonObjectDelivery callback){
    String Url = ApiEndPoints.GET_STORIES;
    AndroidNetworking.get(Url)
        .setPriority(Priority.HIGH)
        .addHeaders("Authorization", "Bearer " + 
            loginInfo.getString(SharedPreferencesKeys.JWT_TOKEN, "null"))
        .build()
        .getAsJSONObject(new JSONObjectRequestListener() {
            @Override
            public void onResponse(JSONObject response) {
                callback.onSuccess(response);
            }
            
            @Override
            public void onError(ANError anError) {
                callback.onError(anError.getMessage());
            }
        });
}

Get User’s Stories

public void getStoriesOf(String Userid, 
                        NetworkCallbackInterfaceWithJsonObjectDelivery callback){
    String Url = ApiEndPoints.GET_STORIES + Userid;
    AndroidNetworking.get(Url)
        .setPriority(Priority.HIGH)
        .addHeaders("Authorization", "Bearer " + 
            loginInfo.getString(SharedPreferencesKeys.JWT_TOKEN, "null"))
        .build()
        .getAsJSONObject(new JSONObjectRequestListener() {
            @Override
            public void onResponse(JSONObject response) {
                callback.onSuccess(response);
            }
        });
}

Get My Stories

public void getMyStories(
    NetworkCallbackInterfaceWithJsonObjectDelivery callback){
    String Url = ApiEndPoints.GET_MY_STORIES;
    AndroidNetworking.get(Url)
        .setPriority(Priority.HIGH)
        .addHeaders("Authorization", "Bearer " + 
            loginInfo.getString(SharedPreferencesKeys.JWT_TOKEN, "null"))
        .build()
        .getAsJSONObject(new JSONObjectRequestListener() {
            @Override
            public void onResponse(JSONObject response) {
                callback.onSuccess(response);
            }
        });
}

Deleting Stories

network_managers/StoriesManager.java
public void RemoveStory(int storyId, NetworkCallbackInterface callbackInterface){
    String URL = ApiEndPoints.DELETE_STORY + Integer.toString(storyId);
    AndroidNetworking.delete(URL)
        .addHeaders("Authorization", "Bearer " + 
            loginInfo.getString(SharedPreferencesKeys.JWT_TOKEN, null))
        .setPriority(Priority.HIGH)
        .build()
        .getAsJSONObject(new JSONObjectRequestListener() {
            @Override
            public void onResponse(JSONObject response) {
                callbackInterface.onSuccess();
            }
            
            @Override
            public void onError(ANError anError) {
                callbackInterface.onError(anError.toString());
            }
        });
}

Story Viewer UI

Stories are viewed in a full-screen ViewPager:
// Story viewer with swipe navigation
StoriesViewpagerAdapter adapter = new StoriesViewpagerAdapter(
    this, 
    storyList, 
    currentPosition
);
viewPager.setAdapter(adapter);
viewPager.setCurrentItem(currentPosition);

Story Playback

Video stories use ExoPlayer with no-loop mode:
fragments/storiesFragment/UploadStoryFinalFragment.java
// Play story video
ExoplayerUtil.playNoLoop(Uri.parse(storyUrl), playerView);

// Auto-advance to next story when video ends
exoplayer.addListener(new Player.Listener() {
    @Override
    public void onPlaybackStateChanged(int state) {
        if (state == Player.STATE_ENDED) {
            // Move to next story
            viewPager.setCurrentItem(viewPager.getCurrentItem() + 1);
        }
    }
});

Story Progress Indicators

Multiple stories from one user show progress bars:
// Show progress bars for each story segment
LinearLayout progressBarsContainer = findViewById(R.id.progress_bars);
for (int i = 0; i < storyCount; i++) {
    ProgressBar progressBar = new ProgressBar(this, null, 
        android.R.attr.progressBarStyleHorizontal);
    progressBar.setMax(100);
    progressBarsContainer.addView(progressBar);
}

// Animate current story progress
ValueAnimator animator = ValueAnimator.ofInt(0, 100);
animator.setDuration(5000); // 5 seconds per story
animator.addUpdateListener(animation -> {
    int progress = (int) animation.getAnimatedValue();
    currentProgressBar.setProgress(progress);
});
animator.start();

Story Model

models/StoryMediaModel.java
public class StoryMediaModel {
    private int storyId;
    private String userid;
    private String username;
    private String profilePic;
    private String mediaUrl;
    private String type; // "image" or "video"
    private String creationTime;
    private boolean isSeen;
    
    // Getters and setters
}

Stories ViewModel

viewmodels/StoriesViewModel.java
public class StoriesViewModel extends ViewModel {
    private MutableLiveData<List<StoriesModel>> stories;
    private MutableLiveData<List<StoryMediaModel>> myStories;
    private StoriesManager storiesManager;
    
    public void loadStories() {
        storiesManager.getStories(
            new NetworkCallbackInterfaceWithJsonObjectDelivery() {
            @Override
            public void onSuccess(JSONObject response) {
                // Parse and update LiveData
                List<StoriesModel> storyList = parseStories(response);
                stories.postValue(storyList);
            }
        });
    }
    
    public void loadMyStories() {
        storiesManager.getMyStories(
            new NetworkCallbackInterfaceWithJsonObjectDelivery() {
            @Override
            public void onSuccess(JSONObject response) {
                List<StoryMediaModel> myStoryList = parseMyStories(response);
                myStories.postValue(myStoryList);
            }
        });
    }
    
    public LiveData<List<StoriesModel>> getStories() {
        return stories;
    }
    
    public LiveData<List<StoryMediaModel>> getMyStories() {
        return myStories;
    }
}

Story Callback Interface

interfaces/StoryOpenCallback.java
public interface StoryOpenCallback {
    void openStoryOf(String userid, String profilePic, 
                    List<StoryMediaModel> stories, int position);
}

Visual Indicators

Unviewed Stories

Stories you haven’t seen yet are highlighted with a colored ring (gradient or solid color).

Viewed Stories

Stories you’ve already watched appear with a gray ring.

Your Story

Your story shows your profile picture with a + icon if you haven’t posted today.

Active Story

Your active story shows a colored ring and “Your story” label.

Story Features

Tap Controls

  • Tap right: Next story
  • Tap left: Previous story
  • Long press: Pause playback
  • Swipe down: Close story viewer

Story Options

For your own stories:
  • Delete story
  • View story statistics
  • Share story
For others’ stories:
  • Reply to story
  • Share story
  • Report story

Best Practices

Stories automatically expire after 24 hours. The backend handles deletion of expired stories.
Preload the next story while the current one is playing for seamless transitions.

Build docs developers (and LLMs) love